pagination curseur, layout 3 colonnes article, sidebar fixe
@@ -0,0 +1,97 @@
|
|||||||
|
<svg width="100%" viewBox="0 0 680 520" role="img" xmlns="http://www.w3.org/2000/svg" style="">
|
||||||
|
<title style="fill:rgb(0, 0, 0);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">ImageMagick sur Debian : convert-im6, magick, et leur lien avec les binaires sous-jacents</title>
|
||||||
|
<desc style="fill:rgb(0, 0, 0);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">Schéma illustrant comment les commandes convert et magick sont des symlinks vers les binaires versionnés convert-im6.q16 et magick-im7.q16 sur Debian.</desc>
|
||||||
|
<defs>
|
||||||
|
<marker id="arrow" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse"><path d="M2 1L8 5L2 9" fill="none" stroke="context-stroke" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></marker>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<text x="340" y="36" text-anchor="middle" font-size="16" style="fill:rgb(20, 20, 19);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:14px;font-weight:500;text-anchor:middle;dominant-baseline:auto">ImageMagick sur Debian</text>
|
||||||
|
<text x="340" y="56" text-anchor="middle" style="fill:rgb(61, 61, 58);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:12px;font-weight:400;text-anchor:middle;dominant-baseline:auto">Ce que tu tapes, ce que le système exécute</text>
|
||||||
|
|
||||||
|
<rect x="40" y="90" width="290" height="180" rx="8" fill="none" stroke="var(--color-border-tertiary)" stroke-width="0.5" stroke-dasharray="4 3" style="fill:none;stroke:rgba(31, 30, 29, 0.15);color:rgb(0, 0, 0);stroke-width:0.5px;stroke-dasharray:4px, 3px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||||
|
<text x="60" y="112" style="fill:rgb(61, 61, 58);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:12px;font-weight:400;text-anchor:start;dominant-baseline:auto">Debian 11 / 12 (bookworm)</text>
|
||||||
|
|
||||||
|
<g style="fill:rgb(0, 0, 0);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
|
||||||
|
<rect x="80" y="130" width="120" height="44" rx="8" style="fill:rgb(230, 241, 251);stroke:rgb(24, 95, 165);color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||||
|
<text x="140" y="152" text-anchor="middle" style="fill:rgb(12, 68, 124);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:14px;font-weight:500;text-anchor:middle;dominant-baseline:auto">convert</text>
|
||||||
|
<text x="140" y="168" text-anchor="middle" style="fill:rgb(24, 95, 165);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:12px;font-weight:400;text-anchor:middle;dominant-baseline:auto">tu tapes</text>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<line x1="200" y1="152" x2="240" y2="152" stroke="var(--color-text-tertiary)" stroke-width="0.5" marker-end="url(#arrow)" stroke-dasharray="3 2" style="fill:none;stroke:rgb(115, 114, 108);color:rgb(0, 0, 0);stroke-width:1.5px;stroke-dasharray:3px, 2px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||||
|
|
||||||
|
<g style="fill:rgb(0, 0, 0);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
|
||||||
|
<rect x="240" y="130" width="70" height="44" rx="8" style="fill:rgb(241, 239, 232);stroke:rgb(95, 94, 90);color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||||
|
<text x="275" y="148" text-anchor="middle" style="fill:rgb(68, 68, 65);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:14px;font-weight:400;text-anchor:middle;dominant-baseline:auto">indispo</text>
|
||||||
|
<text x="275" y="162" text-anchor="middle" style="fill:rgb(95, 94, 90);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:12px;font-weight:400;text-anchor:middle;dominant-baseline:auto">par défaut</text>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<g style="fill:rgb(0, 0, 0);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
|
||||||
|
<rect x="80" y="200" width="120" height="44" rx="8" style="fill:rgb(230, 241, 251);stroke:rgb(24, 95, 165);color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||||
|
<text x="140" y="222" text-anchor="middle" style="fill:rgb(12, 68, 124);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:14px;font-weight:500;text-anchor:middle;dominant-baseline:auto">convert-im6</text>
|
||||||
|
<text x="140" y="238" text-anchor="middle" style="fill:rgb(24, 95, 165);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:12px;font-weight:400;text-anchor:middle;dominant-baseline:auto">tu tapes</text>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<line x1="200" y1="222" x2="240" y2="222" stroke="var(--color-text-secondary)" stroke-width="0.5" marker-end="url(#arrow)" style="fill:none;stroke:rgb(115, 114, 108);color:rgb(0, 0, 0);stroke-width:1.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||||
|
|
||||||
|
<g style="fill:rgb(0, 0, 0);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
|
||||||
|
<rect x="240" y="200" width="80" height="44" rx="8" style="fill:rgb(225, 245, 238);stroke:rgb(15, 110, 86);color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||||
|
<text x="280" y="220" text-anchor="middle" style="fill:rgb(8, 80, 65);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:14px;font-weight:500;text-anchor:middle;dominant-baseline:auto">IM 6</text>
|
||||||
|
<text x="280" y="234" text-anchor="middle" style="fill:rgb(15, 110, 86);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:12px;font-weight:400;text-anchor:middle;dominant-baseline:auto">.q16</text>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<rect x="350" y="90" width="290" height="180" rx="8" fill="none" stroke="var(--color-border-tertiary)" stroke-width="0.5" stroke-dasharray="4 3" style="fill:none;stroke:rgba(31, 30, 29, 0.15);color:rgb(0, 0, 0);stroke-width:0.5px;stroke-dasharray:4px, 3px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||||
|
<text x="370" y="112" style="fill:rgb(61, 61, 58);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:12px;font-weight:400;text-anchor:start;dominant-baseline:auto">Debian 13 (trixie)</text>
|
||||||
|
|
||||||
|
<g style="fill:rgb(0, 0, 0);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
|
||||||
|
<rect x="380" y="130" width="120" height="44" rx="8" style="fill:rgb(230, 241, 251);stroke:rgb(24, 95, 165);color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||||
|
<text x="440" y="152" text-anchor="middle" style="fill:rgb(12, 68, 124);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:14px;font-weight:500;text-anchor:middle;dominant-baseline:auto">convert</text>
|
||||||
|
<text x="440" y="168" text-anchor="middle" style="fill:rgb(24, 95, 165);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:12px;font-weight:400;text-anchor:middle;dominant-baseline:auto">symlink</text>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<line x1="500" y1="152" x2="540" y2="152" stroke="var(--color-text-secondary)" stroke-width="0.5" marker-end="url(#arrow)" style="fill:none;stroke:rgb(115, 114, 108);color:rgb(0, 0, 0);stroke-width:1.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||||
|
|
||||||
|
<g style="fill:rgb(0, 0, 0);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
|
||||||
|
<rect x="540" y="130" width="80" height="44" rx="8" style="fill:rgb(250, 238, 218);stroke:rgb(133, 79, 11);color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||||
|
<text x="580" y="150" text-anchor="middle" style="fill:rgb(99, 56, 6);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:14px;font-weight:500;text-anchor:middle;dominant-baseline:auto">IM 7</text>
|
||||||
|
<text x="580" y="164" text-anchor="middle" style="fill:rgb(133, 79, 11);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:12px;font-weight:400;text-anchor:middle;dominant-baseline:auto">.q16</text>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<g style="fill:rgb(0, 0, 0);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
|
||||||
|
<rect x="380" y="200" width="120" height="44" rx="8" style="fill:rgb(230, 241, 251);stroke:rgb(24, 95, 165);color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||||
|
<text x="440" y="222" text-anchor="middle" style="fill:rgb(12, 68, 124);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:14px;font-weight:500;text-anchor:middle;dominant-baseline:auto">magick</text>
|
||||||
|
<text x="440" y="238" text-anchor="middle" style="fill:rgb(24, 95, 165);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:12px;font-weight:400;text-anchor:middle;dominant-baseline:auto">recommandé</text>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<line x1="500" y1="222" x2="540" y2="222" stroke="var(--color-text-secondary)" stroke-width="0.5" marker-end="url(#arrow)" style="fill:none;stroke:rgb(115, 114, 108);color:rgb(0, 0, 0);stroke-width:1.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||||
|
|
||||||
|
<g style="fill:rgb(0, 0, 0);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
|
||||||
|
<rect x="540" y="200" width="80" height="44" rx="8" style="fill:rgb(250, 238, 218);stroke:rgb(133, 79, 11);color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||||
|
<text x="580" y="220" text-anchor="middle" style="fill:rgb(99, 56, 6);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:14px;font-weight:500;text-anchor:middle;dominant-baseline:auto">IM 7</text>
|
||||||
|
<text x="580" y="234" text-anchor="middle" style="fill:rgb(133, 79, 11);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:12px;font-weight:400;text-anchor:middle;dominant-baseline:auto">.q16</text>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<text x="340" y="310" text-anchor="middle" style="fill:rgb(20, 20, 19);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:14px;font-weight:500;text-anchor:middle;dominant-baseline:auto">Pourquoi ce renommage ?</text>
|
||||||
|
|
||||||
|
<g style="fill:rgb(0, 0, 0);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
|
||||||
|
<rect x="100" y="335" width="200" height="80" rx="8" style="fill:rgb(225, 245, 238);stroke:rgb(15, 110, 86);color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||||
|
<text x="200" y="362" text-anchor="middle" style="fill:rgb(8, 80, 65);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:14px;font-weight:500;text-anchor:middle;dominant-baseline:auto">ImageMagick 6</text>
|
||||||
|
<text x="200" y="382" text-anchor="middle" style="fill:rgb(15, 110, 86);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:12px;font-weight:400;text-anchor:middle;dominant-baseline:auto">convert, identify,</text>
|
||||||
|
<text x="200" y="398" text-anchor="middle" style="fill:rgb(15, 110, 86);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:12px;font-weight:400;text-anchor:middle;dominant-baseline:auto">mogrify, composite</text>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<g style="fill:rgb(0, 0, 0);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
|
||||||
|
<rect x="380" y="335" width="200" height="80" rx="8" style="fill:rgb(250, 238, 218);stroke:rgb(133, 79, 11);color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||||
|
<text x="480" y="362" text-anchor="middle" style="fill:rgb(99, 56, 6);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:14px;font-weight:500;text-anchor:middle;dominant-baseline:auto">ImageMagick 7</text>
|
||||||
|
<text x="480" y="382" text-anchor="middle" style="fill:rgb(133, 79, 11);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:12px;font-weight:400;text-anchor:middle;dominant-baseline:auto">magick</text>
|
||||||
|
<text x="480" y="398" text-anchor="middle" style="fill:rgb(133, 79, 11);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:12px;font-weight:400;text-anchor:middle;dominant-baseline:auto">(commande unique)</text>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<text x="340" y="365" text-anchor="middle" style="fill:rgb(20, 20, 19);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:14px;font-weight:400;text-anchor:middle;dominant-baseline:auto">+</text>
|
||||||
|
<text x="340" y="385" text-anchor="middle" style="fill:rgb(61, 61, 58);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:12px;font-weight:400;text-anchor:middle;dominant-baseline:auto">noms en</text>
|
||||||
|
<text x="340" y="400" text-anchor="middle" style="fill:rgb(61, 61, 58);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:12px;font-weight:400;text-anchor:middle;dominant-baseline:auto">conflit</text>
|
||||||
|
|
||||||
|
<rect x="100" y="445" width="480" height="50" rx="8" fill="var(--color-background-secondary)" stroke="var(--color-border-tertiary)" stroke-width="0.5" style="fill:rgb(245, 244, 237);stroke:rgba(31, 30, 29, 0.15);color:rgb(0, 0, 0);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||||
|
<text x="340" y="467" text-anchor="middle" style="fill:rgb(20, 20, 19);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:14px;font-weight:400;text-anchor:middle;dominant-baseline:auto">Solution Debian : suffixer chaque binaire</text>
|
||||||
|
<text x="340" y="484" text-anchor="middle" style="fill:rgb(61, 61, 58);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:12px;font-weight:400;text-anchor:middle;dominant-baseline:auto">convert-im6.q16 / magick-im7.q16 → cohabitation possible</text>
|
||||||
|
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 23 KiB |
@@ -0,0 +1,115 @@
|
|||||||
|
<svg width="100%" viewBox="0 0 690 490" role="img" xmlns="http://www.w3.org/2000/svg" style="">
|
||||||
|
<title style="fill:rgb(0, 0, 0);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">magick : la commande unifiée d'ImageMagick 7</title>
|
||||||
|
<desc style="fill:rgb(0, 0, 0);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">Illustration représentant la commande magick comme une baguette magique transformant une image source en plusieurs formats.</desc>
|
||||||
|
|
||||||
|
<defs>
|
||||||
|
<radialGradient id="bgGlow" cx="50%" cy="45%" r="60%">
|
||||||
|
<stop offset="0%" stop-color="#2a1f4d"/>
|
||||||
|
<stop offset="60%" stop-color="#1a1530"/>
|
||||||
|
<stop offset="100%" stop-color="#0f0b1f"/>
|
||||||
|
</radialGradient>
|
||||||
|
<radialGradient id="starGlow" cx="50%" cy="50%" r="50%">
|
||||||
|
<stop offset="0%" stop-color="#ffe89a" stop-opacity="1"/>
|
||||||
|
<stop offset="40%" stop-color="#f4a949" stop-opacity="0.8"/>
|
||||||
|
<stop offset="100%" stop-color="#f4a949" stop-opacity="0"/>
|
||||||
|
</radialGradient>
|
||||||
|
<linearGradient id="wand" x1="0" y1="0" x2="1" y2="1">
|
||||||
|
<stop offset="0%" stop-color="#3d2a5c"/>
|
||||||
|
<stop offset="50%" stop-color="#5a3d7a"/>
|
||||||
|
<stop offset="100%" stop-color="#2a1a3d"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="wandTip" x1="0" y1="0" x2="1" y2="1">
|
||||||
|
<stop offset="0%" stop-color="#f4d489"/>
|
||||||
|
<stop offset="100%" stop-color="#c89149"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<rect x="0" y="0" width="680" height="480" fill="url(#bgGlow)" style="stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||||
|
|
||||||
|
<g opacity="0.6" style="fill:rgb(0, 0, 0);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:0.6;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
|
||||||
|
<circle cx="60" cy="80" r="1" fill="#e0d4ff" style="fill:rgb(224, 212, 255);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||||
|
<circle cx="140" cy="50" r="0.8" fill="#e0d4ff" style="fill:rgb(224, 212, 255);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||||
|
<circle cx="220" cy="100" r="1.2" fill="#fff4d4" style="fill:rgb(255, 244, 212);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||||
|
<circle cx="580" cy="60" r="1" fill="#e0d4ff" style="fill:rgb(224, 212, 255);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||||
|
<circle cx="620" cy="120" r="0.8" fill="#fff4d4" style="fill:rgb(255, 244, 212);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||||
|
<circle cx="80" cy="380" r="1" fill="#e0d4ff" style="fill:rgb(224, 212, 255);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||||
|
<circle cx="600" cy="400" r="1.2" fill="#fff4d4" style="fill:rgb(255, 244, 212);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||||
|
<circle cx="40" cy="240" r="0.8" fill="#e0d4ff" style="fill:rgb(224, 212, 255);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||||
|
<circle cx="640" cy="280" r="1" fill="#e0d4ff" style="fill:rgb(224, 212, 255);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||||
|
<circle cx="180" cy="420" r="0.8" fill="#fff4d4" style="fill:rgb(255, 244, 212);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||||
|
<circle cx="500" cy="440" r="1" fill="#e0d4ff" style="fill:rgb(224, 212, 255);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<g transform="translate(120, 200)" style="fill:rgb(0, 0, 0);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
|
||||||
|
<rect x="0" y="0" width="140" height="100" rx="6" fill="#3d2a5c" stroke="#7a5fa3" stroke-width="1.5" style="fill:rgb(61, 42, 92);stroke:rgb(122, 95, 163);color:rgb(0, 0, 0);stroke-width:1.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||||
|
<rect x="6" y="6" width="128" height="88" rx="3" fill="#1a1530" style="fill:rgb(26, 21, 48);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||||
|
|
||||||
|
<circle cx="50" cy="40" r="14" fill="#f4a949" opacity="0.9" style="fill:rgb(244, 169, 73);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:0.9;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||||
|
<path d="M 6 80 L 40 55 L 70 70 L 100 45 L 134 65 L 134 94 L 6 94 Z" fill="#5a3d7a" style="fill:rgb(90, 61, 122);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||||
|
<path d="M 6 85 L 50 70 L 90 80 L 134 75 L 134 94 L 6 94 Z" fill="#7a5fa3" style="fill:rgb(122, 95, 163);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||||
|
|
||||||
|
<text x="70" y="115" text-anchor="middle" fill="#a08fc4" font-family="ui-monospace, monospace" font-size="11" style="fill:rgb(160, 143, 196);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:ui-monospace, monospace;font-size:11px;font-weight:400;text-anchor:middle;dominant-baseline:auto">photo.jpg</text>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<g transform="translate(280, 230)" style="fill:rgb(0, 0, 0);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
|
||||||
|
<line x1="0" y1="0" x2="120" y2="0" stroke="url(#wand)" stroke-width="8" stroke-linecap="round" style="fill:rgb(0, 0, 0);color:rgb(0, 0, 0);stroke-width:8px;stroke-linecap:round;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||||
|
<line x1="0" y1="0" x2="120" y2="0" stroke="#8a6dad" stroke-width="2" stroke-linecap="round" opacity="0.5" style="fill:rgb(0, 0, 0);stroke:rgb(138, 109, 173);color:rgb(0, 0, 0);stroke-width:2px;stroke-linecap:round;stroke-linejoin:miter;opacity:0.5;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||||
|
|
||||||
|
<g transform="translate(130, 0)" style="fill:rgb(0, 0, 0);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
|
||||||
|
<circle cx="0" cy="0" r="35" fill="url(#starGlow)" opacity="0.7" style="stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:0.7;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||||
|
<circle cx="0" cy="0" r="22" fill="url(#starGlow)" style="stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||||
|
|
||||||
|
<g style="fill:rgb(0, 0, 0);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
|
||||||
|
<path d="M 0 -16 L 4 -4 L 16 0 L 4 4 L 0 16 L -4 4 L -16 0 L -4 -4 Z" fill="#fff4d4" style="fill:rgb(255, 244, 212);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||||
|
<path d="M 0 -10 L 2.5 -2.5 L 10 0 L 2.5 2.5 L 0 10 L -2.5 2.5 L -10 0 L -2.5 -2.5 Z" fill="#ffffff" style="fill:rgb(255, 255, 255);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<g fill="#ffe89a" style="fill:rgb(255, 232, 154);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
|
||||||
|
<circle cx="155" cy="-25" r="2" style="fill:rgb(255, 232, 154);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||||
|
<circle cx="170" cy="-10" r="1.5" style="fill:rgb(255, 232, 154);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||||
|
<circle cx="175" cy="15" r="2" style="fill:rgb(255, 232, 154);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||||
|
<circle cx="160" cy="30" r="1.5" style="fill:rgb(255, 232, 154);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||||
|
<circle cx="145" cy="-35" r="1.2" style="fill:rgb(255, 232, 154);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||||
|
<circle cx="190" cy="0" r="1.8" style="fill:rgb(255, 232, 154);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||||
|
<circle cx="180" cy="-25" r="1" style="fill:rgb(255, 232, 154);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<g stroke="#ffe89a" stroke-width="1.5" stroke-linecap="round" opacity="0.7" fill="none" style="fill:none;stroke:rgb(255, 232, 154);color:rgb(0, 0, 0);stroke-width:1.5px;stroke-linecap:round;stroke-linejoin:miter;opacity:0.7;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
|
||||||
|
<path d="M 150 -15 L 153 -22" style="fill:none;stroke:rgb(255, 232, 154);color:rgb(0, 0, 0);stroke-width:1.5px;stroke-linecap:round;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||||
|
<path d="M 165 -5 L 172 -8" style="fill:none;stroke:rgb(255, 232, 154);color:rgb(0, 0, 0);stroke-width:1.5px;stroke-linecap:round;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||||
|
<path d="M 165 12 L 173 18" style="fill:none;stroke:rgb(255, 232, 154);color:rgb(0, 0, 0);stroke-width:1.5px;stroke-linecap:round;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||||
|
<path d="M 150 22 L 152 30" style="fill:none;stroke:rgb(255, 232, 154);color:rgb(0, 0, 0);stroke-width:1.5px;stroke-linecap:round;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<g transform="translate(460, 130)" style="fill:rgb(0, 0, 0);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
|
||||||
|
<rect x="0" y="0" width="100" height="50" rx="5" fill="#2d4a3d" stroke="#7fb069" stroke-width="1.5" style="fill:rgb(45, 74, 61);stroke:rgb(127, 176, 105);color:rgb(0, 0, 0);stroke-width:1.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||||
|
<text x="50" y="22" text-anchor="middle" fill="#7fb069" font-family="ui-monospace, monospace" font-size="11" font-weight="500" style="fill:rgb(127, 176, 105);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:ui-monospace, monospace;font-size:11px;font-weight:500;text-anchor:middle;dominant-baseline:auto">PNG</text>
|
||||||
|
<text x="50" y="38" text-anchor="middle" fill="#a8d089" font-family="ui-monospace, monospace" font-size="9" style="fill:rgb(168, 208, 137);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:ui-monospace, monospace;font-size:9px;font-weight:400;text-anchor:middle;dominant-baseline:auto">photo.png</text>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<g transform="translate(460, 215)" style="fill:rgb(0, 0, 0);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
|
||||||
|
<rect x="0" y="0" width="100" height="50" rx="5" fill="#4a3d2a" stroke="#e8b059" stroke-width="1.5" style="fill:rgb(74, 61, 42);stroke:rgb(232, 176, 89);color:rgb(0, 0, 0);stroke-width:1.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||||
|
<text x="50" y="22" text-anchor="middle" fill="#e8b059" font-family="ui-monospace, monospace" font-size="11" font-weight="500" style="fill:rgb(232, 176, 89);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:ui-monospace, monospace;font-size:11px;font-weight:500;text-anchor:middle;dominant-baseline:auto">WEBP</text>
|
||||||
|
<text x="50" y="38" text-anchor="middle" fill="#f4d489" font-family="ui-monospace, monospace" font-size="9" style="fill:rgb(244, 212, 137);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:ui-monospace, monospace;font-size:9px;font-weight:400;text-anchor:middle;dominant-baseline:auto">photo.webp</text>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<g transform="translate(460, 300)" style="fill:rgb(0, 0, 0);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
|
||||||
|
<rect x="0" y="0" width="100" height="50" rx="5" fill="#4a2a3d" stroke="#d47a9a" stroke-width="1.5" style="fill:rgb(74, 42, 61);stroke:rgb(212, 122, 154);color:rgb(0, 0, 0);stroke-width:1.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||||
|
<text x="50" y="22" text-anchor="middle" fill="#d47a9a" font-family="ui-monospace, monospace" font-size="11" font-weight="500" style="fill:rgb(212, 122, 154);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:ui-monospace, monospace;font-size:11px;font-weight:500;text-anchor:middle;dominant-baseline:auto">AVIF</text>
|
||||||
|
<text x="50" y="38" text-anchor="middle" fill="#e8a4bc" font-family="ui-monospace, monospace" font-size="9" style="fill:rgb(232, 164, 188);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:ui-monospace, monospace;font-size:9px;font-weight:400;text-anchor:middle;dominant-baseline:auto">photo.avif</text>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<g stroke="#7a5fa3" stroke-width="1" fill="none" opacity="0.5" stroke-dasharray="3 3" style="fill:none;stroke:rgb(122, 95, 163);color:rgb(0, 0, 0);stroke-width:1px;stroke-dasharray:3px, 3px;stroke-linecap:butt;stroke-linejoin:miter;opacity:0.5;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
|
||||||
|
<path d="M 425 240 Q 445 200 460 155" style="fill:none;stroke:rgb(122, 95, 163);color:rgb(0, 0, 0);stroke-width:1px;stroke-dasharray:3px, 3px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||||
|
<path d="M 425 240 L 460 240" style="fill:none;stroke:rgb(122, 95, 163);color:rgb(0, 0, 0);stroke-width:1px;stroke-dasharray:3px, 3px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||||
|
<path d="M 425 240 Q 445 280 460 325" style="fill:none;stroke:rgb(122, 95, 163);color:rgb(0, 0, 0);stroke-width:1px;stroke-dasharray:3px, 3px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<text x="340" y="60" text-anchor="middle" fill="#e0d4ff" font-family="var(--font-sans)" font-size="32" font-weight="500" letter-spacing="2" style="fill:rgb(224, 212, 255);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:Anthropic Sans, sans-serif;font-size:32px;font-weight:500;text-anchor:middle;dominant-baseline:auto">magick</text>
|
||||||
|
<text x="340" y="84" text-anchor="middle" fill="#a08fc4" font-family="ui-monospace, monospace" font-size="12" style="fill:rgb(160, 143, 196);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:ui-monospace, monospace;font-size:12px;font-weight:400;text-anchor:middle;dominant-baseline:auto">one command, every format</text>
|
||||||
|
|
||||||
|
<text x="340" y="430" text-anchor="middle" fill="#7a5fa3" font-family="ui-monospace, monospace" font-size="11" style="fill:rgb(122, 95, 163);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:ui-monospace, monospace;font-size:11px;font-weight:400;text-anchor:middle;dominant-baseline:auto">$ magick photo.jpg -resize 1600 photo.webp</text>
|
||||||
|
<text x="340" y="450" text-anchor="middle" fill="#5a4a7a" font-family="var(--font-sans)" font-size="11" style="fill:rgb(90, 74, 122);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:Anthropic Sans, sans-serif;font-size:11px;font-weight:400;text-anchor:middle;dominant-baseline:auto">ImageMagick 7</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 24 KiB |
@@ -1,183 +1,78 @@
|
|||||||
ImageMagick est depuis de nombreuses années un outil incontournable pour la manipulation d’images en ligne de commande. Il est utilisé aussi bien dans des scripts simples que dans des chaînes de traitement industrielles, des environnements serveurs ou des pipelines CI/CD.
|
Si tu as déjà installé ImageMagick sur un serveur Debian, tu es probablement tombé sur cette étrangeté : la commande `convert` historique est là, mais elle s'appelle `convert-im6`. Et la commande moderne `magick`, présente partout ailleurs, semble manquer à l'appel — sauf si tu es sur Debian 13, où elle est revenue.
|
||||||
|
|
||||||
Cependant, sous Debian (et ses dérivés comme Ubuntu), l’utilisation d’ImageMagick peut prêter à confusion : la commande historique `convert` n’est pas disponible telle quelle, remplacée par `convert-im6`, et la commande moderne `magick` semble absente.
|
Le sujet est un peu plus subtil qu'il n'y paraît, et beaucoup d'explications qui circulent sur le web sont fausses (notamment celle qui prétend que `convert` entrerait en conflit avec un binaire de `util-linux` — c'est un mythe). Voilà ce qui se passe réellement.
|
||||||
|
|
||||||
Cet article clarifie les raisons de ce choix, les implications techniques, et les bonnes pratiques à adopter.
|
## Un peu de contexte sur ImageMagick
|
||||||
|
|
||||||
---
|
ImageMagick, c'est une suite d'outils en ligne de commande pour manipuler des images : conversion de formats, redimensionnement, compression, génération de vignettes, watermarks, lecture de métadonnées… Le genre d'outil qu'on retrouve aussi bien dans un script bash de cinq lignes que dans une chaîne de traitement industrielle ou un pipeline CI.
|
||||||
|
|
||||||
## 1. ImageMagick : rappel sur l’architecture
|
Historiquement, la suite est composée de plusieurs binaires distincts, chacun avec son rôle : `convert` pour la conversion, `identify` pour lire les métadonnées, `mogrify` pour le traitement par lot, `composite` pour combiner des images, `montage` pour les planches. C'est l'architecture d'**ImageMagick 6**, la version qui a régné en maître pendant une bonne quinzaine d'années.
|
||||||
|
|
||||||
ImageMagick est une suite d’outils permettant :
|
Depuis 2016, **ImageMagick 7** est disponible. Le grand changement, c'est qu'il unifie tout derrière une seule commande : `magick`. Les anciennes commandes deviennent des sous-commandes (`magick convert`, `magick identify`, etc.), même si pour la rétrocompatibilité un binaire `magick` peut continuer à se comporter comme `convert` quand on l'appelle avec une syntaxe d'IM6.
|
||||||
|
|
||||||
* la conversion de formats d’images,
|
## Pourquoi le suffixe `-im6` sur Debian
|
||||||
* le redimensionnement,
|
|
||||||
* la compression,
|
|
||||||
* l’analyse des métadonnées,
|
|
||||||
* le traitement par lot,
|
|
||||||
* la génération d’images (watermarks, miniatures, montages, etc.).
|
|
||||||
|
|
||||||
Historiquement, chaque fonction correspondait à un binaire distinct :
|
C'est ici que beaucoup d'articles racontent n'importe quoi. La vraie raison n'a rien à voir avec un conflit avec `util-linux` — je l'ai vérifié, aucun paquet système ne fournit de commande `convert`. Tu peux le vérifier toi-même : `dpkg -S /usr/bin/convert` ne renvoie rien qui vienne de util-linux.
|
||||||
|
|
||||||
* `convert`
|
La vraie raison est plus prosaïque. Pendant des années, Debian a voulu pouvoir **packager IM6 et IM7 en parallèle** dans la même distribution, pour permettre une transition en douceur. Le souci, c'est que les deux versions fournissent des binaires aux mêmes noms (`convert`, `identify`, `mogrify`…) avec des comportements légèrement différents. Impossible de les installer côte à côte sans renommer.
|
||||||
* `identify`
|
|
||||||
* `mogrify`
|
|
||||||
* `montage`
|
|
||||||
* `composite`
|
|
||||||
|
|
||||||
Cette architecture correspond à **ImageMagick 6**.
|
La solution adoptée par les mainteneurs Debian a été d'ajouter un suffixe explicite au nom de chaque binaire :
|
||||||
|
|
||||||
---
|
- les outils d'IM6 deviennent `convert-im6.q16`, `identify-im6.q16`, etc.
|
||||||
|
- les outils d'IM7 deviennent `magick-im7.q16` et compagnie
|
||||||
|
|
||||||
## 2. Pourquoi Debian n’utilise pas `convert`
|
Le `.q16` indique la profondeur quantique du binaire (16 bits par canal, la valeur par défaut), et le `-im6` / `-im7` indique la version d'ImageMagick. Les noms classiques (`convert`, `magick`) ne sont alors que des symlinks gérés par `update-alternatives`, qui pointent vers la version active. C'est le même mécanisme que pour `java`, `editor`, ou `python` à une époque.
|
||||||
|
|
||||||
Sous Debian, la commande `convert` appartient déjà au paquet **util-linux**.
|
## Ce qui change entre Debian 11, 12 et 13
|
||||||
Elle sert à convertir des systèmes de fichiers (par exemple FAT vers ext4).
|
|
||||||
|
|
||||||
Pour éviter toute ambiguïté et collision binaire, Debian a fait le choix de :
|
C'est l'autre point que la plupart des articles ratent : la situation n'est pas la même selon la version de Debian.
|
||||||
|
|
||||||
* **ne pas fournir `convert` pour ImageMagick**
|
Sur **Debian 11 (bullseye) et 12 (bookworm)**, le paquet `imagemagick` installe IM6 (version 6.9.11.60). Tu n'as que `convert-im6` et ses copains, et `magick` n'existe pas dans les dépôts officiels (le paquet `imagemagick-7.q16` existe mais n'est pas le défaut). C'est cette situation que décrivent la plupart des tutoriels qui traînent sur le web.
|
||||||
* renommer les binaires ImageMagick avec le suffixe `-im6`
|
|
||||||
|
|
||||||
Ainsi, sous Debian :
|
Sur **Debian 13 (trixie)**, sorti en août 2025, le défaut a basculé sur IM7 (version 7.1.1.43). La commande `magick` est disponible, et `convert` est désormais un symlink vers `magick-im7.q16`. Tu peux le vérifier :
|
||||||
|
|
||||||
| Fonction | Binaire |
|
```bash
|
||||||
| ------------------ | --------------- |
|
$ readlink -f /usr/bin/convert
|
||||||
| Conversion d’image | `convert-im6` |
|
/usr/bin/magick-im7.q16
|
||||||
| Identification | `identify-im6` |
|
|
||||||
| Traitement par lot | `mogrify-im6` |
|
|
||||||
| Composition | `composite-im6` |
|
|
||||||
|
|
||||||
Ce choix garantit la stabilité du système et évite toute confusion avec des outils bas niveau.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. ImageMagick 6 vs ImageMagick 7
|
|
||||||
|
|
||||||
### ImageMagick 6 (Debian)
|
|
||||||
|
|
||||||
* Architecture historique
|
|
||||||
* Commandes séparées
|
|
||||||
* Très stable
|
|
||||||
* Largement utilisée en production
|
|
||||||
* Maintenance conservatrice
|
|
||||||
|
|
||||||
Exemple :
|
|
||||||
|
|
||||||
```
|
|
||||||
convert-im6 image.jpg image.png
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
Autrement dit, sur Trixie, si tu écris `convert image.jpg image.png`, tu appelles en réalité IM7 sous un nom d'IM6. Ça fonctionne pour la plupart des usages, mais attention : IM7 est plus strict sur l'ordre des arguments en ligne de commande (`magick [INPUT-OPTIONS] INPUT [OUTPUT-OPTIONS] OUTPUT`), donc certains scripts anciens peuvent grogner.
|
||||||
|
|
||||||
### ImageMagick 7
|
## Correspondance entre les deux versions
|
||||||
|
|
||||||
ImageMagick 7 introduit une évolution majeure :
|
| ImageMagick 6 (Debian 11/12) | ImageMagick 7 (Debian 13) |
|
||||||
👉 **une commande unique : `magick`**
|
| ---------------------------- | ------------------------- |
|
||||||
|
| `convert-im6` | `magick` |
|
||||||
|
| `identify-im6` | `magick identify` |
|
||||||
|
| `mogrify-im6` | `magick mogrify` |
|
||||||
|
| `composite-im6` | `magick composite` |
|
||||||
|
|
||||||
Toutes les fonctionnalités sont regroupées derrière ce binaire.
|
Pour les cas simples, le comportement est identique. Une commande de redimensionnement classique passe sans modification :
|
||||||
|
|
||||||
Exemple :
|
```bash
|
||||||
|
# Debian 11/12
|
||||||
```
|
|
||||||
magick image.jpg image.png
|
|
||||||
```
|
|
||||||
|
|
||||||
Avantages :
|
|
||||||
|
|
||||||
* syntaxe unifiée
|
|
||||||
* meilleure gestion mémoire
|
|
||||||
* prise en charge améliorée des formats modernes (WebP, AVIF, HEIF)
|
|
||||||
* comportement plus cohérent dans les scripts
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Pourquoi `magick` n’est pas disponible par défaut sous Debian
|
|
||||||
|
|
||||||
Debian privilégie :
|
|
||||||
|
|
||||||
* la stabilité à long terme,
|
|
||||||
* la compatibilité ascendante,
|
|
||||||
* les mises à jour maîtrisées.
|
|
||||||
|
|
||||||
ImageMagick 7, bien que mature, est considéré comme une rupture de comportement par rapport à la version 6.
|
|
||||||
Pour cette raison, Debian stable fournit uniquement **ImageMagick 6**, et donc **pas la commande `magick`**.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. Correspondance entre IM6 et IM7
|
|
||||||
|
|
||||||
| ImageMagick 6 (Debian) | ImageMagick 7 |
|
|
||||||
| ---------------------- | ------------------ |
|
|
||||||
| `convert-im6` | `magick` |
|
|
||||||
| `identify-im6` | `magick identify` |
|
|
||||||
| `mogrify-im6` | `magick mogrify` |
|
|
||||||
| `composite-im6` | `magick composite` |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. Exemple concret de conversion
|
|
||||||
|
|
||||||
### Sous Debian (IM6)
|
|
||||||
|
|
||||||
```
|
|
||||||
convert-im6 photo.jpg -resize 1600x1600 photo_reduite.jpg
|
convert-im6 photo.jpg -resize 1600x1600 photo_reduite.jpg
|
||||||
```
|
|
||||||
|
|
||||||
### Sous IM7
|
# Debian 13
|
||||||
|
|
||||||
```
|
|
||||||
magick photo.jpg -resize 1600x1600 photo_reduite.jpg
|
magick photo.jpg -resize 1600x1600 photo_reduite.jpg
|
||||||
```
|
```
|
||||||
|
|
||||||
Le comportement est identique.
|
## Faut-il s'inquiéter sur un serveur en production ?
|
||||||
|
|
||||||
---
|
Si tu administres une machine Debian 12 ou plus ancienne, non. IM6 est toujours activement maintenu pour les CVE (les correctifs sont régulièrement backportés dans les paquets stable), et la plupart des scripts existants continueront de fonctionner. Le `-im6` dans le nom du binaire est juste du marquage, pas une dépréciation.
|
||||||
|
|
||||||
## 7. Faut-il installer ImageMagick 7 sur Debian ?
|
Si tu migres vers Debian 13, prévois un peu de temps pour relire tes scripts. Les pièges classiques :
|
||||||
|
|
||||||
### Cas où cela peut être justifié
|
- l'ordre des options qui devient plus strict ;
|
||||||
|
- quelques comportements de couleur et d'alpha qui ont changé entre les deux versions, notamment sur les opérations chaînées ;
|
||||||
|
- le fichier `policy.xml` qui a déménagé : `/etc/ImageMagick-6/` devient `/etc/ImageMagick-7/`. Si tu avais assoupli les restrictions sur les PDF ou PostScript (un grand classique), il faut reporter la modification.
|
||||||
|
|
||||||
* Besoin explicite de `magick`
|
Pour un projet PHP comme les tiens, l'extension Imagick côté PHP est sensible à cette transition : la version compilée doit correspondre à la version d'IM installée, sinon `pecl install imagick` échoue. Sur Trixie, c'est IM7 qu'il faut lier.
|
||||||
* Utilisation intensive de formats modernes (AVIF, HEIF)
|
|
||||||
* Développement d’outils multi-plateformes
|
|
||||||
* Environnement isolé (conteneur, VM, CI)
|
|
||||||
|
|
||||||
### Cas où il vaut mieux rester en IM6
|
## En pratique
|
||||||
|
|
||||||
* Serveur de production
|
Sur Debian 11/12, utilise `convert-im6`, `identify-im6`, etc. C'est la convention locale, pas une version dégradée. Si tu veux `magick` malgré tout, tu peux installer le paquet `imagemagick-7.q16` (présent dans les dépôts depuis bookworm) et basculer les alternatives manuellement, mais ce n'est presque jamais nécessaire.
|
||||||
* Scripts existants
|
|
||||||
* Environnement stable long terme
|
|
||||||
* Administration système classique
|
|
||||||
|
|
||||||
---
|
Sur Debian 13, utilise `magick` directement. La commande `convert` reste disponible par compatibilité, mais elle pointe en réalité vers IM7 — autant utiliser le nom officiel.
|
||||||
|
|
||||||
## 8. Recommandation opérationnelle
|
Et dans tous les cas, évite les alias globaux qui réécrivent `convert` : ça finit toujours par mordre quelqu'un, soit toi dans six mois, soit le prochain qui reprendra le serveur.
|
||||||
|
|
||||||
Sur Debian :
|
|
||||||
|
|
||||||
* ✔ Utiliser `convert-im6` et les outils associés
|
|
||||||
* ✔ S’appuyer sur la stabilité d’ImageMagick 6
|
|
||||||
* ✔ Éviter les alias globaux modifiant `convert`
|
|
||||||
* ❌ Ne pas forcer l’installation d’IM7 sans besoin réel
|
|
||||||
|
|
||||||
Pour un environnement de développement ou de test, IM7 peut être installé séparément sans conflit, à condition de ne pas écraser les binaires système.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 9. Conclusion
|
|
||||||
|
|
||||||
La présence de `convert-im6` au lieu de `magick` n’est ni une anomalie ni une régression :
|
|
||||||
c’est un **choix volontaire de Debian**, dicté par la stabilité et la cohérence du système.
|
|
||||||
|
|
||||||
ImageMagick 6 reste parfaitement adapté à la majorité des usages professionnels.
|
|
||||||
ImageMagick 7 apporte une ergonomie moderne, mais n’est pas indispensable dans la majorité des cas.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### En résumé
|
|
||||||
|
|
||||||
* `convert-im6` → normal sous Debian
|
|
||||||
* `magick` → ImageMagick 7 uniquement
|
|
||||||
* Les deux sont fonctionnellement équivalents
|
|
||||||
* Le choix dépend du contexte, pas d’une supériorité technique absolue
|
|
||||||
@@ -1,13 +1,34 @@
|
|||||||
{
|
{
|
||||||
"uuid": "0e0b8d1d-3352-4ab7-bc70-7bc1f02ee485",
|
"uuid": "0e0b8d1d-3352-4ab7-bc70-7bc1f02ee485",
|
||||||
"slug": "imagemagick-sous-debian-comprendre-convert-im6-magick-et-les-choix-de-conception",
|
"slug": "imagemagick-sur-debian-pourquoi-convert-im6-et-ou-trouver-magick",
|
||||||
"title": "ImageMagick sous Debian : comprendre `convert-im6`, `magick` et les choix de conception",
|
"title": "ImageMagick sur Debian : pourquoi `convert-im6` et où trouver `magick`",
|
||||||
"author": "cedric@abonnel.fr",
|
"author": "cedric@abonnel.fr",
|
||||||
"published": true,
|
"published": true,
|
||||||
"published_at": "2025-12-28 15:32:01",
|
"published_at": "2025-12-28 15:32",
|
||||||
"created_at": "2025-12-28 15:32:01",
|
"created_at": "2025-12-28 15:32:01",
|
||||||
"updated_at": "2025-12-28 15:32:01",
|
"updated_at": "2026-05-12 00:29:00",
|
||||||
"revisions": [],
|
"revisions": [
|
||||||
"cover": "cover.jpg",
|
{
|
||||||
|
"n": 1,
|
||||||
|
"date": "2026-05-12 00:29:00",
|
||||||
|
"comment": "",
|
||||||
|
"title": "ImageMagick sur Debian : pourquoi `convert-im6` et où trouver `magick`"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cover": "cover.svg",
|
||||||
|
"files_meta": {
|
||||||
|
"45de275c5f174797-24653.svg": {
|
||||||
|
"author": "",
|
||||||
|
"source_url": ""
|
||||||
|
},
|
||||||
|
"7ee02eb092b334c4-23450.svg": {
|
||||||
|
"author": "",
|
||||||
|
"source_url": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"external_links": [],
|
||||||
|
"seo_title": "",
|
||||||
|
"seo_description": "",
|
||||||
|
"og_image": "",
|
||||||
"category": "linux"
|
"category": "linux"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
Si tu as déjà installé ImageMagick sur un serveur Debian, tu es probablement tombé sur cette étrangeté : la commande `convert` historique est là, mais elle s'appelle `convert-im6`. Et la commande moderne `magick`, présente partout ailleurs, semble manquer à l'appel — sauf si tu es sur Debian 13, où elle est revenue.
|
||||||
|
|
||||||
|
Le sujet est un peu plus subtil qu'il n'y paraît, et beaucoup d'explications qui circulent sur le web sont fausses (notamment celle qui prétend que `convert` entrerait en conflit avec un binaire de `util-linux` — c'est un mythe). Voilà ce qui se passe réellement.
|
||||||
|
|
||||||
|
## Un peu de contexte sur ImageMagick
|
||||||
|
|
||||||
|
ImageMagick, c'est une suite d'outils en ligne de commande pour manipuler des images : conversion de formats, redimensionnement, compression, génération de vignettes, watermarks, lecture de métadonnées… Le genre d'outil qu'on retrouve aussi bien dans un script bash de cinq lignes que dans une chaîne de traitement industrielle ou un pipeline CI.
|
||||||
|
|
||||||
|
Historiquement, la suite est composée de plusieurs binaires distincts, chacun avec son rôle : `convert` pour la conversion, `identify` pour lire les métadonnées, `mogrify` pour le traitement par lot, `composite` pour combiner des images, `montage` pour les planches. C'est l'architecture d'**ImageMagick 6**, la version qui a régné en maître pendant une bonne quinzaine d'années.
|
||||||
|
|
||||||
|
Depuis 2016, **ImageMagick 7** est disponible. Le grand changement, c'est qu'il unifie tout derrière une seule commande : `magick`. Les anciennes commandes deviennent des sous-commandes (`magick convert`, `magick identify`, etc.), même si pour la rétrocompatibilité un binaire `magick` peut continuer à se comporter comme `convert` quand on l'appelle avec une syntaxe d'IM6.
|
||||||
|
|
||||||
|
## Pourquoi le suffixe `-im6` sur Debian
|
||||||
|
|
||||||
|
C'est ici que beaucoup d'articles racontent n'importe quoi. La vraie raison n'a rien à voir avec un conflit avec `util-linux` — je l'ai vérifié, aucun paquet système ne fournit de commande `convert`. Tu peux le vérifier toi-même : `dpkg -S /usr/bin/convert` ne renvoie rien qui vienne de util-linux.
|
||||||
|
|
||||||
|
La vraie raison est plus prosaïque. Pendant des années, Debian a voulu pouvoir **packager IM6 et IM7 en parallèle** dans la même distribution, pour permettre une transition en douceur. Le souci, c'est que les deux versions fournissent des binaires aux mêmes noms (`convert`, `identify`, `mogrify`…) avec des comportements légèrement différents. Impossible de les installer côte à côte sans renommer.
|
||||||
|
|
||||||
|
La solution adoptée par les mainteneurs Debian a été d'ajouter un suffixe explicite au nom de chaque binaire :
|
||||||
|
|
||||||
|
- les outils d'IM6 deviennent `convert-im6.q16`, `identify-im6.q16`, etc.
|
||||||
|
- les outils d'IM7 deviennent `magick-im7.q16` et compagnie
|
||||||
|
|
||||||
|
Le `.q16` indique la profondeur quantique du binaire (16 bits par canal, la valeur par défaut), et le `-im6` / `-im7` indique la version d'ImageMagick. Les noms classiques (`convert`, `magick`) ne sont alors que des symlinks gérés par `update-alternatives`, qui pointent vers la version active. C'est le même mécanisme que pour `java`, `editor`, ou `python` à une époque.
|
||||||
|
|
||||||
|
## Ce qui change entre Debian 11, 12 et 13
|
||||||
|
|
||||||
|
C'est l'autre point que la plupart des articles ratent : la situation n'est pas la même selon la version de Debian.
|
||||||
|
|
||||||
|
Sur **Debian 11 (bullseye) et 12 (bookworm)**, le paquet `imagemagick` installe IM6 (version 6.9.11.60). Tu n'as que `convert-im6` et ses copains, et `magick` n'existe pas dans les dépôts officiels (le paquet `imagemagick-7.q16` existe mais n'est pas le défaut). C'est cette situation que décrivent la plupart des tutoriels qui traînent sur le web.
|
||||||
|
|
||||||
|
Sur **Debian 13 (trixie)**, sorti en août 2025, le défaut a basculé sur IM7 (version 7.1.1.43). La commande `magick` est disponible, et `convert` est désormais un symlink vers `magick-im7.q16`. Tu peux le vérifier :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ readlink -f /usr/bin/convert
|
||||||
|
/usr/bin/magick-im7.q16
|
||||||
|
```
|
||||||
|
|
||||||
|
Autrement dit, sur Trixie, si tu écris `convert image.jpg image.png`, tu appelles en réalité IM7 sous un nom d'IM6. Ça fonctionne pour la plupart des usages, mais attention : IM7 est plus strict sur l'ordre des arguments en ligne de commande (`magick [INPUT-OPTIONS] INPUT [OUTPUT-OPTIONS] OUTPUT`), donc certains scripts anciens peuvent grogner.
|
||||||
|
|
||||||
|
## Correspondance entre les deux versions
|
||||||
|
|
||||||
|
| ImageMagick 6 (Debian 11/12) | ImageMagick 7 (Debian 13) |
|
||||||
|
| ---------------------------- | ------------------------- |
|
||||||
|
| `convert-im6` | `magick` |
|
||||||
|
| `identify-im6` | `magick identify` |
|
||||||
|
| `mogrify-im6` | `magick mogrify` |
|
||||||
|
| `composite-im6` | `magick composite` |
|
||||||
|
|
||||||
|
Pour les cas simples, le comportement est identique. Une commande de redimensionnement classique passe sans modification :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Debian 11/12
|
||||||
|
convert-im6 photo.jpg -resize 1600x1600 photo_reduite.jpg
|
||||||
|
|
||||||
|
# Debian 13
|
||||||
|
magick photo.jpg -resize 1600x1600 photo_reduite.jpg
|
||||||
|
```
|
||||||
|
|
||||||
|
## Faut-il s'inquiéter sur un serveur en production ?
|
||||||
|
|
||||||
|
Si tu administres une machine Debian 12 ou plus ancienne, non. IM6 est toujours activement maintenu pour les CVE (les correctifs sont régulièrement backportés dans les paquets stable), et la plupart des scripts existants continueront de fonctionner. Le `-im6` dans le nom du binaire est juste du marquage, pas une dépréciation.
|
||||||
|
|
||||||
|
Si tu migres vers Debian 13, prévois un peu de temps pour relire tes scripts. Les pièges classiques :
|
||||||
|
|
||||||
|
- l'ordre des options qui devient plus strict ;
|
||||||
|
- quelques comportements de couleur et d'alpha qui ont changé entre les deux versions, notamment sur les opérations chaînées ;
|
||||||
|
- le fichier `policy.xml` qui a déménagé : `/etc/ImageMagick-6/` devient `/etc/ImageMagick-7/`. Si tu avais assoupli les restrictions sur les PDF ou PostScript (un grand classique), il faut reporter la modification.
|
||||||
|
|
||||||
|
Pour un projet PHP comme les tiens, l'extension Imagick côté PHP est sensible à cette transition : la version compilée doit correspondre à la version d'IM installée, sinon `pecl install imagick` échoue. Sur Trixie, c'est IM7 qu'il faut lier.
|
||||||
|
|
||||||
|
## En pratique
|
||||||
|
|
||||||
|
Sur Debian 11/12, utilise `convert-im6`, `identify-im6`, etc. C'est la convention locale, pas une version dégradée. Si tu veux `magick` malgré tout, tu peux installer le paquet `imagemagick-7.q16` (présent dans les dépôts depuis bookworm) et basculer les alternatives manuellement, mais ce n'est presque jamais nécessaire.
|
||||||
|
|
||||||
|
Sur Debian 13, utilise `magick` directement. La commande `convert` reste disponible par compatibilité, mais elle pointe en réalité vers IM7 — autant utiliser le nom officiel.
|
||||||
|
|
||||||
|
Et dans tous les cas, évite les alias globaux qui réécrivent `convert` : ça finit toujours par mordre quelqu'un, soit toi dans six mois, soit le prochain qui reprendra le serveur.
|
||||||
|
After Width: | Height: | Size: 386 KiB |
@@ -1,43 +1,43 @@
|
|||||||
## 1. Présentation générale
|
## 1. À quoi ça sert
|
||||||
|
|
||||||
ImageMagick est une suite d’outils en ligne de commande permettant de **créer, convertir, modifier et analyser des images**.
|
ImageMagick, c'est l'outil qu'on sort quand on veut manipuler des images sans ouvrir un logiciel graphique. Pas de Photoshop, pas de GIMP, pas de clic-droit "Redimensionner" sur cent fichiers à la suite : juste une commande dans un terminal, et le travail est fait.
|
||||||
Il supporte plus de 200 formats (JPEG, PNG, WebP, TIFF, PDF, SVG, HEIC, etc.) et fonctionne sans interface graphique, ce qui le rend idéal pour :
|
|
||||||
|
|
||||||
* les serveurs,
|
C'est une suite d'outils qui sait lire, écrire et transformer plus de 200 formats — du JPEG classique au PDF en passant par le HEIC des iPhones, le WebP de Google ou le bon vieux TIFF des scanners. L'absence d'interface graphique est ici une fonctionnalité, pas un défaut : elle permet de l'utiliser partout où il n'y a pas d'écran, et surtout dans tout ce qui doit tourner tout seul.
|
||||||
* les scripts automatisés,
|
|
||||||
* les pipelines CI/CD,
|
|
||||||
* les traitements en masse,
|
|
||||||
* les environnements sans interface graphique (SSH, Docker, etc.).
|
|
||||||
|
|
||||||
Depuis la version 7, la commande principale est `magick`, qui remplace progressivement les anciennes commandes (`convert`, `identify`, `mogrify`, etc.).
|
On le retrouve donc naturellement :
|
||||||
|
|
||||||
---
|
- sur des serveurs web qui génèrent des miniatures à la volée,
|
||||||
|
- dans des scripts qui traitent des dossiers entiers d'un coup,
|
||||||
|
- dans des pipelines CI/CD pour préparer des assets,
|
||||||
|
- dans des conteneurs Docker, accessibles uniquement en SSH.
|
||||||
|
|
||||||
|
Depuis la version 7, tout passe par une commande unique : `magick`. Les anciennes commandes (`convert`, `identify`, `mogrify`...) existent toujours pour la compatibilité, mais elles ne sont plus la norme.
|
||||||
|
|
||||||
## 2. Installation
|
## 2. Installation
|
||||||
|
|
||||||
Sous Debian / Ubuntu :
|
Sur Debian ou Ubuntu :
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo apt install imagemagick
|
sudo apt install imagemagick
|
||||||
```
|
```
|
||||||
|
|
||||||
Vérification :
|
On vérifie ensuite que tout est en place :
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
magick -version
|
magick -version
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
La sortie indique aussi les délégués compilés (libwebp, libheif, libraw, etc.). Si un format précis vous intéresse, c'est ici qu'il faut regarder : ImageMagick ne sait lire un format que si la bibliothèque correspondante est présente au moment de la compilation.
|
||||||
|
|
||||||
## 3. Principe fondamental de fonctionnement
|
## 3. Comment ImageMagick raisonne
|
||||||
|
|
||||||
ImageMagick fonctionne selon une logique simple :
|
Toutes les commandes suivent la même logique :
|
||||||
|
|
||||||
```
|
```
|
||||||
magick [entrée] [options] [sortie]
|
magick [entrée] [options] [sortie]
|
||||||
```
|
```
|
||||||
|
|
||||||
Chaque option modifie l’image en mémoire, dans l’ordre où elle est écrite.
|
L'image est chargée en mémoire, puis chaque option s'applique **dans l'ordre où elle est écrite**, comme une chaîne de traitement. Ce point est important : déplacer une option dans la ligne peut changer le résultat final.
|
||||||
|
|
||||||
Exemple :
|
Exemple :
|
||||||
|
|
||||||
@@ -45,144 +45,134 @@ Exemple :
|
|||||||
magick input.jpg -resize 800x600 -quality 85 output.jpg
|
magick input.jpg -resize 800x600 -quality 85 output.jpg
|
||||||
```
|
```
|
||||||
|
|
||||||
➡️ L’image est chargée → redimensionnée → compressée → enregistrée.
|
Ici, l'image est lue, redimensionnée à 800×600, puis compressée à 85% de qualité, puis écrite sur le disque. Si on inversait `-quality` et `-resize`, le résultat serait identique dans ce cas précis, mais avec des opérations qui modifient les pixels (flou, conversion d'espace colorimétrique, recadrage), l'ordre devient critique.
|
||||||
|
|
||||||
---
|
## 4. Convertir d'un format à un autre
|
||||||
|
|
||||||
## 4. Conversion de format
|
Le cas le plus simple : changer l'extension du fichier de sortie suffit.
|
||||||
|
|
||||||
### Conversion simple
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
magick image.png image.jpg
|
magick image.png image.jpg
|
||||||
```
|
```
|
||||||
|
|
||||||
Cela convertit automatiquement le format selon l’extension du fichier de sortie.
|
ImageMagick détecte le format cible à partir de l'extension et fait la conversion. C'est aussi simple que ça pour 90% des cas.
|
||||||
|
|
||||||
### Conversion avec changement de profondeur de couleur
|
Quand on veut être plus précis — par exemple forcer une profondeur de couleur particulière — on l'indique explicitement :
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
magick image.png -depth 8 image.jpg
|
magick image.png -depth 8 image.jpg
|
||||||
```
|
```
|
||||||
|
|
||||||
Utile pour réduire la taille ou assurer une compatibilité.
|
Utile quand on récupère des images en 16 bits par canal qu'on veut ramener à du 8 bits standard, soit pour gagner de la place, soit pour garantir la compatibilité avec un logiciel récalcitrant.
|
||||||
|
|
||||||
---
|
## 5. Redimensionner
|
||||||
|
|
||||||
## 5. Redimensionnement des images
|
### La méthode brutale
|
||||||
|
|
||||||
### Redimensionnement simple
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
magick image.jpg -resize 800x600 image_resized.jpg
|
magick image.jpg -resize 800x600 image_resized.jpg
|
||||||
```
|
```
|
||||||
|
|
||||||
⚠️ Cela force exactement ces dimensions, quitte à déformer l’image.
|
Cette commande redimensionne à 800×600 **en respectant les proportions** par défaut, contrairement à ce qu'on pourrait croire. Si l'image source est en 4:3, elle rentrera pile dedans ; si elle est en 16:9, ImageMagick choisira la dimension la plus contraignante et l'autre sera plus petite que demandé.
|
||||||
|
|
||||||
---
|
Pour forcer exactement ces dimensions quitte à déformer l'image, il faut ajouter un point d'exclamation :
|
||||||
|
|
||||||
### Conserver les proportions (recommandé)
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
magick image.jpg -resize 800x600\> image_resized.jpg
|
magick image.jpg -resize 800x600! image_resized.jpg
|
||||||
```
|
```
|
||||||
|
|
||||||
Le `>` signifie :
|
### Ne rétrécir que les grandes images
|
||||||
|
|
||||||
> Ne redimensionner que si l’image dépasse ces dimensions.
|
C'est probablement le cas le plus utile au quotidien : on a un dossier d'images, on veut s'assurer qu'aucune ne dépasse 1600 pixels, mais on ne veut pas agrandir les petites (ce qui dégraderait leur qualité).
|
||||||
|
|
||||||
---
|
```bash
|
||||||
|
magick image.jpg -resize "1600x1600>" image_resized.jpg
|
||||||
|
```
|
||||||
|
|
||||||
### Redimensionnement par pourcentage
|
Le `>` signifie « uniquement si l'image est plus grande ». Les guillemets sont nécessaires car `>` est interprété par le shell comme une redirection. On peut aussi échapper le caractère avec `\>`.
|
||||||
|
|
||||||
|
### En pourcentage
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
magick image.jpg -resize 50% image_small.jpg
|
magick image.jpg -resize 50% image_small.jpg
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
Pratique quand on veut diviser la taille par deux sans calculer les dimensions exactes.
|
||||||
|
|
||||||
## 6. Qualité et compression
|
## 6. Qualité et poids du fichier
|
||||||
|
|
||||||
### Réduire la taille du fichier JPEG
|
Pour les JPEG, le paramètre `-quality` règle le compromis entre fidélité visuelle et poids du fichier :
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
magick image.jpg -quality 85 image.jpg
|
magick image.jpg -quality 85 image.jpg
|
||||||
```
|
```
|
||||||
|
|
||||||
* 100 : qualité maximale
|
Quelques repères en pratique :
|
||||||
* 80–85 : bon compromis web
|
|
||||||
* <70 : perte visible
|
|
||||||
|
|
||||||
---
|
- **100** : qualité maximale, fichier énorme, différence imperceptible avec 95.
|
||||||
|
- **85** : la valeur par défaut de la plupart des appareils photo, et un excellent compromis pour le web.
|
||||||
|
- **75** : encore très acceptable, gain de place notable.
|
||||||
|
- **En dessous de 70** : les artefacts deviennent visibles, surtout sur les aplats de couleur.
|
||||||
|
|
||||||
### Suppression des métadonnées (EXIF, GPS, etc.)
|
### Supprimer les métadonnées
|
||||||
|
|
||||||
|
Les fichiers issus d'appareils photo ou de smartphones embarquent beaucoup d'informations : modèle de l'appareil, date, parfois coordonnées GPS, miniature intégrée, profil colorimétrique... Tout ça peut peser plusieurs dizaines de kilo-octets, et surtout poser des problèmes de confidentialité.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
magick image.jpg -strip image.jpg
|
magick image.jpg -strip image.jpg
|
||||||
```
|
```
|
||||||
|
|
||||||
Très utile pour :
|
L'option `-strip` fait le ménage. À utiliser systématiquement avant de publier des photos sur le web, et indispensable dès qu'on parle de RGPD ou d'anonymisation. Attention en revanche pour la photographie professionnelle où certaines métadonnées (droits d'auteur, profil ICC) peuvent être nécessaires.
|
||||||
|
|
||||||
* anonymisation
|
## 7. Recadrer et adapter à un cadre
|
||||||
* gain de place
|
|
||||||
* conformité RGPD
|
|
||||||
|
|
||||||
---
|
### Recadrage classique
|
||||||
|
|
||||||
## 7. Redimensionnement intelligent (crop & resize)
|
|
||||||
|
|
||||||
### Recadrer une image
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
magick image.jpg -crop 800x600+100+50 output.jpg
|
magick image.jpg -crop 800x600+100+50 output.jpg
|
||||||
```
|
```
|
||||||
|
|
||||||
Format :
|
La syntaxe se lit comme une fenêtre qu'on découpe dans l'image : largeur × hauteur, décalée de 100 pixels depuis la gauche et 50 pixels depuis le haut.
|
||||||
|
|
||||||
```
|
### Remplir un cadre exact, sans déformation
|
||||||
largeur x hauteur + décalage_x + décalage_y
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
C'est le besoin typique des miniatures de site : on veut toutes les vignettes en 800×600 pile, peu importe le format des photos d'origine.
|
||||||
|
|
||||||
### Remplir un cadre sans déformation
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
magick image.jpg -resize 800x600^ -gravity center -extent 800x600 output.jpg
|
magick image.jpg -resize 800x600^ -gravity center -extent 800x600 output.jpg
|
||||||
```
|
```
|
||||||
|
|
||||||
Cela :
|
Trois étapes enchaînées :
|
||||||
|
|
||||||
* redimensionne en conservant les proportions
|
1. `-resize 800x600^` redimensionne pour que l'image **remplisse** le cadre (le `^` inverse la logique habituelle : on prend la plus grande dimension comme contrainte, pas la plus petite).
|
||||||
* coupe l’excédent centré
|
2. `-gravity center` indique qu'on veut centrer le découpage.
|
||||||
|
3. `-extent 800x600` coupe ce qui dépasse pour obtenir exactement la taille voulue.
|
||||||
|
|
||||||
Très utilisé pour les miniatures.
|
Le résultat : aucune déformation, aucune bande noire, juste un éventuel rognage sur les bords les plus longs.
|
||||||
|
|
||||||
---
|
## 8. Traiter un dossier entier
|
||||||
|
|
||||||
## 8. Traitement par lot
|
Une boucle Bash suffit pour convertir tous les PNG d'un dossier en JPEG :
|
||||||
|
|
||||||
### Conversion de tous les fichiers d’un dossier
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
for f in *.jpg; do
|
for f in *.png; do
|
||||||
magick "$f" "${f%.jpg}.png"
|
magick "$f" "${f%.png}.jpg"
|
||||||
done
|
done
|
||||||
```
|
```
|
||||||
|
|
||||||
### Redimensionnement massif (avec écrasement)
|
La syntaxe `${f%.png}` retire l'extension `.png` du nom, on y ajoute `.jpg`. Simple et fiable.
|
||||||
|
|
||||||
|
Pour modifier les fichiers **sur place**, ImageMagick fournit `mogrify` :
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
mogrify -resize 1600x1600\> *.jpg
|
mogrify -resize "1600x1600>" *.jpg
|
||||||
```
|
```
|
||||||
|
|
||||||
⚠️ `mogrify` modifie les fichiers originaux.
|
Cette commande écrase chaque fichier par sa version redimensionnée. C'est rapide et pratique, mais ça veut aussi dire qu'**il n'y a pas de retour en arrière** : si la commande est mal écrite, le dossier original est perdu. Règle absolue : travailler sur une copie, ou s'assurer d'avoir une sauvegarde.
|
||||||
|
|
||||||
---
|
## 9. Texte et filigranes
|
||||||
|
|
||||||
## 9. Ajout de texte et filigranes
|
### Apposer une mention textuelle
|
||||||
|
|
||||||
### Ajouter un texte
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
magick image.jpg \
|
magick image.jpg \
|
||||||
@@ -193,83 +183,82 @@ magick image.jpg \
|
|||||||
image_marked.jpg
|
image_marked.jpg
|
||||||
```
|
```
|
||||||
|
|
||||||
### Ajouter un watermark (image)
|
`-gravity` ancre le texte dans un coin de l'image (les neuf positions classiques : `northwest`, `north`, `northeast`, `west`, `center`...), et `-annotate` ajoute un décalage par rapport à ce point d'ancrage. Ici, `+10+10` éloigne le texte de 10 pixels du coin inférieur droit.
|
||||||
|
|
||||||
|
### Superposer un logo ou un watermark image
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
magick image.jpg watermark.png -gravity center -composite output.jpg
|
magick image.jpg watermark.png -gravity center -composite output.jpg
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
L'image principale est lue en premier, le filigrane en second, puis `-composite` les fusionne. Si le watermark a un canal alpha (transparence), il est respecté.
|
||||||
|
|
||||||
## 10. Gestion des couleurs
|
## 10. Couleurs et tons
|
||||||
|
|
||||||
### Convertir en niveaux de gris
|
Passage en noir et blanc :
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
magick image.jpg -colorspace Gray output.jpg
|
magick image.jpg -colorspace Gray output.jpg
|
||||||
```
|
```
|
||||||
|
|
||||||
### Ajuster luminosité / contraste
|
Réglage de la luminosité et du contraste (valeurs en pourcentage, positives ou négatives) :
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
magick image.jpg -brightness-contrast 10x5 output.jpg
|
magick image.jpg -brightness-contrast 10x5 output.jpg
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
Ici, +10% de luminosité et +5% de contraste. Pour assombrir, on utilise des valeurs négatives : `-brightness-contrast -10x0`.
|
||||||
|
|
||||||
## 11. Informations sur une image
|
## 11. Inspecter une image
|
||||||
|
|
||||||
### Détails complets
|
Pour obtenir les informations essentielles — format, dimensions, profondeur :
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
magick identify image.jpg
|
magick identify image.jpg
|
||||||
```
|
```
|
||||||
|
|
||||||
### Informations étendues
|
Pour tout savoir, y compris les métadonnées EXIF, le profil colorimétrique, l'histogramme :
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
magick identify -verbose image.jpg
|
magick identify -verbose image.jpg
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
La sortie verbeuse peut faire plusieurs pages, mais c'est inestimable pour diagnostiquer un problème ou comprendre d'où vient un fichier.
|
||||||
|
|
||||||
## 12. Formats modernes
|
## 12. Formats modernes
|
||||||
|
|
||||||
### Conversion vers WebP
|
Le WebP de Google offre une compression nettement meilleure que le JPEG à qualité équivalente, et il est aujourd'hui supporté par tous les navigateurs courants :
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
magick image.jpg -quality 80 image.webp
|
magick image.jpg -quality 80 image.webp
|
||||||
```
|
```
|
||||||
|
|
||||||
### Conversion vers AVIF
|
L'AVIF va encore plus loin en termes de compression, au prix d'un encodage plus lent :
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
magick image.jpg image.avif
|
magick image.jpg image.avif
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
Si la commande échoue avec une erreur de délégué, c'est que votre installation d'ImageMagick a été compilée sans le support AVIF — il faudra installer `libheif` ou recompiler.
|
||||||
|
|
||||||
## 13. Bonnes pratiques
|
## 13. Quelques règles à se fixer
|
||||||
|
|
||||||
* Toujours tester sur une copie
|
- **Toujours travailler sur une copie** quand on découvre une nouvelle commande. `mogrify` en particulier ne pardonne pas.
|
||||||
* Éviter `mogrify` sans sauvegarde
|
- **Stripper les métadonnées** avant toute publication web.
|
||||||
* Utiliser `-strip` pour le web
|
- **Pour de très gros volumes** (plusieurs milliers d'images, ou des images très lourdes), regarder du côté de `libvips` : c'est plus rapide et beaucoup moins gourmand en mémoire qu'ImageMagick. Pour tout le reste, ImageMagick est largement suffisant.
|
||||||
* Privilégier `libvips` pour très gros volumes
|
- **Automatiser dès qu'on répète** : si la même commande revient deux fois, elle mérite un script.
|
||||||
* Automatiser via scripts Bash
|
- **Lire les messages d'erreur** : ImageMagick est verbeux, et la plupart des problèmes (délégué manquant, permissions, format non reconnu) sont explicitement nommés dans la sortie.
|
||||||
|
|
||||||
|
## 14. Là où on le croise vraiment
|
||||||
|
|
||||||
|
En pratique, ImageMagick finit presque toujours dans les mêmes situations :
|
||||||
|
|
||||||
|
- préparation d'images pour un site web (redimensionnement + compression + strip),
|
||||||
|
- génération de miniatures à la volée côté serveur,
|
||||||
|
- normalisation d'un catalogue photo hétérogène (formats, tailles, profils),
|
||||||
|
- conversion massive d'archives anciennes vers des formats modernes,
|
||||||
|
- nettoyage des métadonnées avant diffusion publique.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 14. Cas d’usage typiques
|
ImageMagick fait partie de ces outils qu'on apprivoise lentement mais qu'on garde longtemps. Au début, on copie des commandes trouvées en ligne sans tout comprendre. Puis on commence à reconnaître les options, à les combiner, à écrire ses propres scripts. Et un jour, on se rend compte qu'on a remplacé un logiciel entier par trois lignes de Bash — et qu'on n'a jamais été aussi efficace pour traiter des images.
|
||||||
|
|
||||||
* Préparation d’images pour un site web
|
|
||||||
* Génération de miniatures
|
|
||||||
* Normalisation d’un catalogue photo
|
|
||||||
* Conversion massive pour archivage
|
|
||||||
* Nettoyage de métadonnées avant publication
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
ImageMagick est un outil extrêmement puissant, capable de remplacer de nombreux logiciels graphiques lorsqu’on maîtrise sa syntaxe.
|
|
||||||
Il s’intègre parfaitement dans des scripts, des serveurs et des chaînes de traitement automatisées.
|
|
||||||
|
|
||||||
Pour un usage professionnel ou intensif, c’est un outil incontournable sous Linux.
|
|
||||||
@@ -1,13 +1,36 @@
|
|||||||
{
|
{
|
||||||
"uuid": "11186836-bbac-4054-82db-a3bfed14a274",
|
"uuid": "11186836-bbac-4054-82db-a3bfed14a274",
|
||||||
"slug": "imagemagick-le-couteau-suisse-de-la-manipulation-d-images-sous-linux",
|
"slug": "imagemagick-traiter-des-images-en-ligne-de-commande",
|
||||||
"title": "ImageMagick : le couteau suisse de la manipulation d’images sous Linux",
|
"title": "ImageMagick : traiter des images en ligne de commande",
|
||||||
"author": "cedric@abonnel.fr",
|
"author": "cedric@abonnel.fr",
|
||||||
"published": true,
|
"published": true,
|
||||||
"published_at": "2025-12-28 14:56:14",
|
"published_at": "2025-12-28 14:56",
|
||||||
"created_at": "2025-12-28 14:56:14",
|
"created_at": "2025-12-28 14:56:14",
|
||||||
"updated_at": "2025-12-28 14:56:14",
|
"updated_at": "2026-05-12 00:36:01",
|
||||||
"revisions": [],
|
"revisions": [
|
||||||
"cover": "cover.jpg",
|
{
|
||||||
|
"n": 1,
|
||||||
|
"date": "2026-05-12 00:33:58",
|
||||||
|
"comment": "",
|
||||||
|
"title": "ImageMagick : traiter des images en ligne de commande"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"n": 2,
|
||||||
|
"date": "2026-05-12 00:36:01",
|
||||||
|
"comment": "",
|
||||||
|
"title": "ImageMagick : traiter des images en ligne de commande"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cover": "cover.png",
|
||||||
|
"files_meta": {
|
||||||
|
"cover.png": {
|
||||||
|
"author": "",
|
||||||
|
"source_url": "https://imagemagick.org/image/logo.png"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"external_links": [],
|
||||||
|
"seo_title": "",
|
||||||
|
"seo_description": "",
|
||||||
|
"og_image": "",
|
||||||
"category": "linux"
|
"category": "linux"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,264 @@
|
|||||||
|
## 1. À quoi ça sert
|
||||||
|
|
||||||
|
ImageMagick, c'est l'outil qu'on sort quand on veut manipuler des images sans ouvrir un logiciel graphique. Pas de Photoshop, pas de GIMP, pas de clic-droit "Redimensionner" sur cent fichiers à la suite : juste une commande dans un terminal, et le travail est fait.
|
||||||
|
|
||||||
|
C'est une suite d'outils qui sait lire, écrire et transformer plus de 200 formats — du JPEG classique au PDF en passant par le HEIC des iPhones, le WebP de Google ou le bon vieux TIFF des scanners. L'absence d'interface graphique est ici une fonctionnalité, pas un défaut : elle permet de l'utiliser partout où il n'y a pas d'écran, et surtout dans tout ce qui doit tourner tout seul.
|
||||||
|
|
||||||
|
On le retrouve donc naturellement :
|
||||||
|
|
||||||
|
- sur des serveurs web qui génèrent des miniatures à la volée,
|
||||||
|
- dans des scripts qui traitent des dossiers entiers d'un coup,
|
||||||
|
- dans des pipelines CI/CD pour préparer des assets,
|
||||||
|
- dans des conteneurs Docker, accessibles uniquement en SSH.
|
||||||
|
|
||||||
|
Depuis la version 7, tout passe par une commande unique : `magick`. Les anciennes commandes (`convert`, `identify`, `mogrify`...) existent toujours pour la compatibilité, mais elles ne sont plus la norme.
|
||||||
|
|
||||||
|
## 2. Installation
|
||||||
|
|
||||||
|
Sur Debian ou Ubuntu :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo apt install imagemagick
|
||||||
|
```
|
||||||
|
|
||||||
|
On vérifie ensuite que tout est en place :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
magick -version
|
||||||
|
```
|
||||||
|
|
||||||
|
La sortie indique aussi les délégués compilés (libwebp, libheif, libraw, etc.). Si un format précis vous intéresse, c'est ici qu'il faut regarder : ImageMagick ne sait lire un format que si la bibliothèque correspondante est présente au moment de la compilation.
|
||||||
|
|
||||||
|
## 3. Comment ImageMagick raisonne
|
||||||
|
|
||||||
|
Toutes les commandes suivent la même logique :
|
||||||
|
|
||||||
|
```
|
||||||
|
magick [entrée] [options] [sortie]
|
||||||
|
```
|
||||||
|
|
||||||
|
L'image est chargée en mémoire, puis chaque option s'applique **dans l'ordre où elle est écrite**, comme une chaîne de traitement. Ce point est important : déplacer une option dans la ligne peut changer le résultat final.
|
||||||
|
|
||||||
|
Exemple :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
magick input.jpg -resize 800x600 -quality 85 output.jpg
|
||||||
|
```
|
||||||
|
|
||||||
|
Ici, l'image est lue, redimensionnée à 800×600, puis compressée à 85% de qualité, puis écrite sur le disque. Si on inversait `-quality` et `-resize`, le résultat serait identique dans ce cas précis, mais avec des opérations qui modifient les pixels (flou, conversion d'espace colorimétrique, recadrage), l'ordre devient critique.
|
||||||
|
|
||||||
|
## 4. Convertir d'un format à un autre
|
||||||
|
|
||||||
|
Le cas le plus simple : changer l'extension du fichier de sortie suffit.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
magick image.png image.jpg
|
||||||
|
```
|
||||||
|
|
||||||
|
ImageMagick détecte le format cible à partir de l'extension et fait la conversion. C'est aussi simple que ça pour 90% des cas.
|
||||||
|
|
||||||
|
Quand on veut être plus précis — par exemple forcer une profondeur de couleur particulière — on l'indique explicitement :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
magick image.png -depth 8 image.jpg
|
||||||
|
```
|
||||||
|
|
||||||
|
Utile quand on récupère des images en 16 bits par canal qu'on veut ramener à du 8 bits standard, soit pour gagner de la place, soit pour garantir la compatibilité avec un logiciel récalcitrant.
|
||||||
|
|
||||||
|
## 5. Redimensionner
|
||||||
|
|
||||||
|
### La méthode brutale
|
||||||
|
|
||||||
|
```bash
|
||||||
|
magick image.jpg -resize 800x600 image_resized.jpg
|
||||||
|
```
|
||||||
|
|
||||||
|
Cette commande redimensionne à 800×600 **en respectant les proportions** par défaut, contrairement à ce qu'on pourrait croire. Si l'image source est en 4:3, elle rentrera pile dedans ; si elle est en 16:9, ImageMagick choisira la dimension la plus contraignante et l'autre sera plus petite que demandé.
|
||||||
|
|
||||||
|
Pour forcer exactement ces dimensions quitte à déformer l'image, il faut ajouter un point d'exclamation :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
magick image.jpg -resize 800x600! image_resized.jpg
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ne rétrécir que les grandes images
|
||||||
|
|
||||||
|
C'est probablement le cas le plus utile au quotidien : on a un dossier d'images, on veut s'assurer qu'aucune ne dépasse 1600 pixels, mais on ne veut pas agrandir les petites (ce qui dégraderait leur qualité).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
magick image.jpg -resize "1600x1600>" image_resized.jpg
|
||||||
|
```
|
||||||
|
|
||||||
|
Le `>` signifie « uniquement si l'image est plus grande ». Les guillemets sont nécessaires car `>` est interprété par le shell comme une redirection. On peut aussi échapper le caractère avec `\>`.
|
||||||
|
|
||||||
|
### En pourcentage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
magick image.jpg -resize 50% image_small.jpg
|
||||||
|
```
|
||||||
|
|
||||||
|
Pratique quand on veut diviser la taille par deux sans calculer les dimensions exactes.
|
||||||
|
|
||||||
|
## 6. Qualité et poids du fichier
|
||||||
|
|
||||||
|
Pour les JPEG, le paramètre `-quality` règle le compromis entre fidélité visuelle et poids du fichier :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
magick image.jpg -quality 85 image.jpg
|
||||||
|
```
|
||||||
|
|
||||||
|
Quelques repères en pratique :
|
||||||
|
|
||||||
|
- **100** : qualité maximale, fichier énorme, différence imperceptible avec 95.
|
||||||
|
- **85** : la valeur par défaut de la plupart des appareils photo, et un excellent compromis pour le web.
|
||||||
|
- **75** : encore très acceptable, gain de place notable.
|
||||||
|
- **En dessous de 70** : les artefacts deviennent visibles, surtout sur les aplats de couleur.
|
||||||
|
|
||||||
|
### Supprimer les métadonnées
|
||||||
|
|
||||||
|
Les fichiers issus d'appareils photo ou de smartphones embarquent beaucoup d'informations : modèle de l'appareil, date, parfois coordonnées GPS, miniature intégrée, profil colorimétrique... Tout ça peut peser plusieurs dizaines de kilo-octets, et surtout poser des problèmes de confidentialité.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
magick image.jpg -strip image.jpg
|
||||||
|
```
|
||||||
|
|
||||||
|
L'option `-strip` fait le ménage. À utiliser systématiquement avant de publier des photos sur le web, et indispensable dès qu'on parle de RGPD ou d'anonymisation. Attention en revanche pour la photographie professionnelle où certaines métadonnées (droits d'auteur, profil ICC) peuvent être nécessaires.
|
||||||
|
|
||||||
|
## 7. Recadrer et adapter à un cadre
|
||||||
|
|
||||||
|
### Recadrage classique
|
||||||
|
|
||||||
|
```bash
|
||||||
|
magick image.jpg -crop 800x600+100+50 output.jpg
|
||||||
|
```
|
||||||
|
|
||||||
|
La syntaxe se lit comme une fenêtre qu'on découpe dans l'image : largeur × hauteur, décalée de 100 pixels depuis la gauche et 50 pixels depuis le haut.
|
||||||
|
|
||||||
|
### Remplir un cadre exact, sans déformation
|
||||||
|
|
||||||
|
C'est le besoin typique des miniatures de site : on veut toutes les vignettes en 800×600 pile, peu importe le format des photos d'origine.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
magick image.jpg -resize 800x600^ -gravity center -extent 800x600 output.jpg
|
||||||
|
```
|
||||||
|
|
||||||
|
Trois étapes enchaînées :
|
||||||
|
|
||||||
|
1. `-resize 800x600^` redimensionne pour que l'image **remplisse** le cadre (le `^` inverse la logique habituelle : on prend la plus grande dimension comme contrainte, pas la plus petite).
|
||||||
|
2. `-gravity center` indique qu'on veut centrer le découpage.
|
||||||
|
3. `-extent 800x600` coupe ce qui dépasse pour obtenir exactement la taille voulue.
|
||||||
|
|
||||||
|
Le résultat : aucune déformation, aucune bande noire, juste un éventuel rognage sur les bords les plus longs.
|
||||||
|
|
||||||
|
## 8. Traiter un dossier entier
|
||||||
|
|
||||||
|
Une boucle Bash suffit pour convertir tous les PNG d'un dossier en JPEG :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
for f in *.png; do
|
||||||
|
magick "$f" "${f%.png}.jpg"
|
||||||
|
done
|
||||||
|
```
|
||||||
|
|
||||||
|
La syntaxe `${f%.png}` retire l'extension `.png` du nom, on y ajoute `.jpg`. Simple et fiable.
|
||||||
|
|
||||||
|
Pour modifier les fichiers **sur place**, ImageMagick fournit `mogrify` :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mogrify -resize "1600x1600>" *.jpg
|
||||||
|
```
|
||||||
|
|
||||||
|
Cette commande écrase chaque fichier par sa version redimensionnée. C'est rapide et pratique, mais ça veut aussi dire qu'**il n'y a pas de retour en arrière** : si la commande est mal écrite, le dossier original est perdu. Règle absolue : travailler sur une copie, ou s'assurer d'avoir une sauvegarde.
|
||||||
|
|
||||||
|
## 9. Texte et filigranes
|
||||||
|
|
||||||
|
### Apposer une mention textuelle
|
||||||
|
|
||||||
|
```bash
|
||||||
|
magick image.jpg \
|
||||||
|
-gravity southeast \
|
||||||
|
-pointsize 24 \
|
||||||
|
-fill white \
|
||||||
|
-annotate +10+10 "© MonSite" \
|
||||||
|
image_marked.jpg
|
||||||
|
```
|
||||||
|
|
||||||
|
`-gravity` ancre le texte dans un coin de l'image (les neuf positions classiques : `northwest`, `north`, `northeast`, `west`, `center`...), et `-annotate` ajoute un décalage par rapport à ce point d'ancrage. Ici, `+10+10` éloigne le texte de 10 pixels du coin inférieur droit.
|
||||||
|
|
||||||
|
### Superposer un logo ou un watermark image
|
||||||
|
|
||||||
|
```bash
|
||||||
|
magick image.jpg watermark.png -gravity center -composite output.jpg
|
||||||
|
```
|
||||||
|
|
||||||
|
L'image principale est lue en premier, le filigrane en second, puis `-composite` les fusionne. Si le watermark a un canal alpha (transparence), il est respecté.
|
||||||
|
|
||||||
|
## 10. Couleurs et tons
|
||||||
|
|
||||||
|
Passage en noir et blanc :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
magick image.jpg -colorspace Gray output.jpg
|
||||||
|
```
|
||||||
|
|
||||||
|
Réglage de la luminosité et du contraste (valeurs en pourcentage, positives ou négatives) :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
magick image.jpg -brightness-contrast 10x5 output.jpg
|
||||||
|
```
|
||||||
|
|
||||||
|
Ici, +10% de luminosité et +5% de contraste. Pour assombrir, on utilise des valeurs négatives : `-brightness-contrast -10x0`.
|
||||||
|
|
||||||
|
## 11. Inspecter une image
|
||||||
|
|
||||||
|
Pour obtenir les informations essentielles — format, dimensions, profondeur :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
magick identify image.jpg
|
||||||
|
```
|
||||||
|
|
||||||
|
Pour tout savoir, y compris les métadonnées EXIF, le profil colorimétrique, l'histogramme :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
magick identify -verbose image.jpg
|
||||||
|
```
|
||||||
|
|
||||||
|
La sortie verbeuse peut faire plusieurs pages, mais c'est inestimable pour diagnostiquer un problème ou comprendre d'où vient un fichier.
|
||||||
|
|
||||||
|
## 12. Formats modernes
|
||||||
|
|
||||||
|
Le WebP de Google offre une compression nettement meilleure que le JPEG à qualité équivalente, et il est aujourd'hui supporté par tous les navigateurs courants :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
magick image.jpg -quality 80 image.webp
|
||||||
|
```
|
||||||
|
|
||||||
|
L'AVIF va encore plus loin en termes de compression, au prix d'un encodage plus lent :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
magick image.jpg image.avif
|
||||||
|
```
|
||||||
|
|
||||||
|
Si la commande échoue avec une erreur de délégué, c'est que votre installation d'ImageMagick a été compilée sans le support AVIF — il faudra installer `libheif` ou recompiler.
|
||||||
|
|
||||||
|
## 13. Quelques règles à se fixer
|
||||||
|
|
||||||
|
- **Toujours travailler sur une copie** quand on découvre une nouvelle commande. `mogrify` en particulier ne pardonne pas.
|
||||||
|
- **Stripper les métadonnées** avant toute publication web.
|
||||||
|
- **Pour de très gros volumes** (plusieurs milliers d'images, ou des images très lourdes), regarder du côté de `libvips` : c'est plus rapide et beaucoup moins gourmand en mémoire qu'ImageMagick. Pour tout le reste, ImageMagick est largement suffisant.
|
||||||
|
- **Automatiser dès qu'on répète** : si la même commande revient deux fois, elle mérite un script.
|
||||||
|
- **Lire les messages d'erreur** : ImageMagick est verbeux, et la plupart des problèmes (délégué manquant, permissions, format non reconnu) sont explicitement nommés dans la sortie.
|
||||||
|
|
||||||
|
## 14. Là où on le croise vraiment
|
||||||
|
|
||||||
|
En pratique, ImageMagick finit presque toujours dans les mêmes situations :
|
||||||
|
|
||||||
|
- préparation d'images pour un site web (redimensionnement + compression + strip),
|
||||||
|
- génération de miniatures à la volée côté serveur,
|
||||||
|
- normalisation d'un catalogue photo hétérogène (formats, tailles, profils),
|
||||||
|
- conversion massive d'archives anciennes vers des formats modernes,
|
||||||
|
- nettoyage des métadonnées avant diffusion publique.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
ImageMagick fait partie de ces outils qu'on apprivoise lentement mais qu'on garde longtemps. Au début, on copie des commandes trouvées en ligne sans tout comprendre. Puis on commence à reconnaître les options, à les combiner, à écrire ses propres scripts. Et un jour, on se rend compte qu'on a remplacé un logiciel entier par trois lignes de Bash — et qu'on n'a jamais été aussi efficace pour traiter des images.
|
||||||
@@ -0,0 +1,264 @@
|
|||||||
|
## 1. À quoi ça sert
|
||||||
|
|
||||||
|
ImageMagick, c'est l'outil qu'on sort quand on veut manipuler des images sans ouvrir un logiciel graphique. Pas de Photoshop, pas de GIMP, pas de clic-droit "Redimensionner" sur cent fichiers à la suite : juste une commande dans un terminal, et le travail est fait.
|
||||||
|
|
||||||
|
C'est une suite d'outils qui sait lire, écrire et transformer plus de 200 formats — du JPEG classique au PDF en passant par le HEIC des iPhones, le WebP de Google ou le bon vieux TIFF des scanners. L'absence d'interface graphique est ici une fonctionnalité, pas un défaut : elle permet de l'utiliser partout où il n'y a pas d'écran, et surtout dans tout ce qui doit tourner tout seul.
|
||||||
|
|
||||||
|
On le retrouve donc naturellement :
|
||||||
|
|
||||||
|
- sur des serveurs web qui génèrent des miniatures à la volée,
|
||||||
|
- dans des scripts qui traitent des dossiers entiers d'un coup,
|
||||||
|
- dans des pipelines CI/CD pour préparer des assets,
|
||||||
|
- dans des conteneurs Docker, accessibles uniquement en SSH.
|
||||||
|
|
||||||
|
Depuis la version 7, tout passe par une commande unique : `magick`. Les anciennes commandes (`convert`, `identify`, `mogrify`...) existent toujours pour la compatibilité, mais elles ne sont plus la norme.
|
||||||
|
|
||||||
|
## 2. Installation
|
||||||
|
|
||||||
|
Sur Debian ou Ubuntu :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo apt install imagemagick
|
||||||
|
```
|
||||||
|
|
||||||
|
On vérifie ensuite que tout est en place :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
magick -version
|
||||||
|
```
|
||||||
|
|
||||||
|
La sortie indique aussi les délégués compilés (libwebp, libheif, libraw, etc.). Si un format précis vous intéresse, c'est ici qu'il faut regarder : ImageMagick ne sait lire un format que si la bibliothèque correspondante est présente au moment de la compilation.
|
||||||
|
|
||||||
|
## 3. Comment ImageMagick raisonne
|
||||||
|
|
||||||
|
Toutes les commandes suivent la même logique :
|
||||||
|
|
||||||
|
```
|
||||||
|
magick [entrée] [options] [sortie]
|
||||||
|
```
|
||||||
|
|
||||||
|
L'image est chargée en mémoire, puis chaque option s'applique **dans l'ordre où elle est écrite**, comme une chaîne de traitement. Ce point est important : déplacer une option dans la ligne peut changer le résultat final.
|
||||||
|
|
||||||
|
Exemple :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
magick input.jpg -resize 800x600 -quality 85 output.jpg
|
||||||
|
```
|
||||||
|
|
||||||
|
Ici, l'image est lue, redimensionnée à 800×600, puis compressée à 85% de qualité, puis écrite sur le disque. Si on inversait `-quality` et `-resize`, le résultat serait identique dans ce cas précis, mais avec des opérations qui modifient les pixels (flou, conversion d'espace colorimétrique, recadrage), l'ordre devient critique.
|
||||||
|
|
||||||
|
## 4. Convertir d'un format à un autre
|
||||||
|
|
||||||
|
Le cas le plus simple : changer l'extension du fichier de sortie suffit.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
magick image.png image.jpg
|
||||||
|
```
|
||||||
|
|
||||||
|
ImageMagick détecte le format cible à partir de l'extension et fait la conversion. C'est aussi simple que ça pour 90% des cas.
|
||||||
|
|
||||||
|
Quand on veut être plus précis — par exemple forcer une profondeur de couleur particulière — on l'indique explicitement :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
magick image.png -depth 8 image.jpg
|
||||||
|
```
|
||||||
|
|
||||||
|
Utile quand on récupère des images en 16 bits par canal qu'on veut ramener à du 8 bits standard, soit pour gagner de la place, soit pour garantir la compatibilité avec un logiciel récalcitrant.
|
||||||
|
|
||||||
|
## 5. Redimensionner
|
||||||
|
|
||||||
|
### La méthode brutale
|
||||||
|
|
||||||
|
```bash
|
||||||
|
magick image.jpg -resize 800x600 image_resized.jpg
|
||||||
|
```
|
||||||
|
|
||||||
|
Cette commande redimensionne à 800×600 **en respectant les proportions** par défaut, contrairement à ce qu'on pourrait croire. Si l'image source est en 4:3, elle rentrera pile dedans ; si elle est en 16:9, ImageMagick choisira la dimension la plus contraignante et l'autre sera plus petite que demandé.
|
||||||
|
|
||||||
|
Pour forcer exactement ces dimensions quitte à déformer l'image, il faut ajouter un point d'exclamation :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
magick image.jpg -resize 800x600! image_resized.jpg
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ne rétrécir que les grandes images
|
||||||
|
|
||||||
|
C'est probablement le cas le plus utile au quotidien : on a un dossier d'images, on veut s'assurer qu'aucune ne dépasse 1600 pixels, mais on ne veut pas agrandir les petites (ce qui dégraderait leur qualité).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
magick image.jpg -resize "1600x1600>" image_resized.jpg
|
||||||
|
```
|
||||||
|
|
||||||
|
Le `>` signifie « uniquement si l'image est plus grande ». Les guillemets sont nécessaires car `>` est interprété par le shell comme une redirection. On peut aussi échapper le caractère avec `\>`.
|
||||||
|
|
||||||
|
### En pourcentage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
magick image.jpg -resize 50% image_small.jpg
|
||||||
|
```
|
||||||
|
|
||||||
|
Pratique quand on veut diviser la taille par deux sans calculer les dimensions exactes.
|
||||||
|
|
||||||
|
## 6. Qualité et poids du fichier
|
||||||
|
|
||||||
|
Pour les JPEG, le paramètre `-quality` règle le compromis entre fidélité visuelle et poids du fichier :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
magick image.jpg -quality 85 image.jpg
|
||||||
|
```
|
||||||
|
|
||||||
|
Quelques repères en pratique :
|
||||||
|
|
||||||
|
- **100** : qualité maximale, fichier énorme, différence imperceptible avec 95.
|
||||||
|
- **85** : la valeur par défaut de la plupart des appareils photo, et un excellent compromis pour le web.
|
||||||
|
- **75** : encore très acceptable, gain de place notable.
|
||||||
|
- **En dessous de 70** : les artefacts deviennent visibles, surtout sur les aplats de couleur.
|
||||||
|
|
||||||
|
### Supprimer les métadonnées
|
||||||
|
|
||||||
|
Les fichiers issus d'appareils photo ou de smartphones embarquent beaucoup d'informations : modèle de l'appareil, date, parfois coordonnées GPS, miniature intégrée, profil colorimétrique... Tout ça peut peser plusieurs dizaines de kilo-octets, et surtout poser des problèmes de confidentialité.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
magick image.jpg -strip image.jpg
|
||||||
|
```
|
||||||
|
|
||||||
|
L'option `-strip` fait le ménage. À utiliser systématiquement avant de publier des photos sur le web, et indispensable dès qu'on parle de RGPD ou d'anonymisation. Attention en revanche pour la photographie professionnelle où certaines métadonnées (droits d'auteur, profil ICC) peuvent être nécessaires.
|
||||||
|
|
||||||
|
## 7. Recadrer et adapter à un cadre
|
||||||
|
|
||||||
|
### Recadrage classique
|
||||||
|
|
||||||
|
```bash
|
||||||
|
magick image.jpg -crop 800x600+100+50 output.jpg
|
||||||
|
```
|
||||||
|
|
||||||
|
La syntaxe se lit comme une fenêtre qu'on découpe dans l'image : largeur × hauteur, décalée de 100 pixels depuis la gauche et 50 pixels depuis le haut.
|
||||||
|
|
||||||
|
### Remplir un cadre exact, sans déformation
|
||||||
|
|
||||||
|
C'est le besoin typique des miniatures de site : on veut toutes les vignettes en 800×600 pile, peu importe le format des photos d'origine.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
magick image.jpg -resize 800x600^ -gravity center -extent 800x600 output.jpg
|
||||||
|
```
|
||||||
|
|
||||||
|
Trois étapes enchaînées :
|
||||||
|
|
||||||
|
1. `-resize 800x600^` redimensionne pour que l'image **remplisse** le cadre (le `^` inverse la logique habituelle : on prend la plus grande dimension comme contrainte, pas la plus petite).
|
||||||
|
2. `-gravity center` indique qu'on veut centrer le découpage.
|
||||||
|
3. `-extent 800x600` coupe ce qui dépasse pour obtenir exactement la taille voulue.
|
||||||
|
|
||||||
|
Le résultat : aucune déformation, aucune bande noire, juste un éventuel rognage sur les bords les plus longs.
|
||||||
|
|
||||||
|
## 8. Traiter un dossier entier
|
||||||
|
|
||||||
|
Une boucle Bash suffit pour convertir tous les PNG d'un dossier en JPEG :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
for f in *.png; do
|
||||||
|
magick "$f" "${f%.png}.jpg"
|
||||||
|
done
|
||||||
|
```
|
||||||
|
|
||||||
|
La syntaxe `${f%.png}` retire l'extension `.png` du nom, on y ajoute `.jpg`. Simple et fiable.
|
||||||
|
|
||||||
|
Pour modifier les fichiers **sur place**, ImageMagick fournit `mogrify` :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mogrify -resize "1600x1600>" *.jpg
|
||||||
|
```
|
||||||
|
|
||||||
|
Cette commande écrase chaque fichier par sa version redimensionnée. C'est rapide et pratique, mais ça veut aussi dire qu'**il n'y a pas de retour en arrière** : si la commande est mal écrite, le dossier original est perdu. Règle absolue : travailler sur une copie, ou s'assurer d'avoir une sauvegarde.
|
||||||
|
|
||||||
|
## 9. Texte et filigranes
|
||||||
|
|
||||||
|
### Apposer une mention textuelle
|
||||||
|
|
||||||
|
```bash
|
||||||
|
magick image.jpg \
|
||||||
|
-gravity southeast \
|
||||||
|
-pointsize 24 \
|
||||||
|
-fill white \
|
||||||
|
-annotate +10+10 "© MonSite" \
|
||||||
|
image_marked.jpg
|
||||||
|
```
|
||||||
|
|
||||||
|
`-gravity` ancre le texte dans un coin de l'image (les neuf positions classiques : `northwest`, `north`, `northeast`, `west`, `center`...), et `-annotate` ajoute un décalage par rapport à ce point d'ancrage. Ici, `+10+10` éloigne le texte de 10 pixels du coin inférieur droit.
|
||||||
|
|
||||||
|
### Superposer un logo ou un watermark image
|
||||||
|
|
||||||
|
```bash
|
||||||
|
magick image.jpg watermark.png -gravity center -composite output.jpg
|
||||||
|
```
|
||||||
|
|
||||||
|
L'image principale est lue en premier, le filigrane en second, puis `-composite` les fusionne. Si le watermark a un canal alpha (transparence), il est respecté.
|
||||||
|
|
||||||
|
## 10. Couleurs et tons
|
||||||
|
|
||||||
|
Passage en noir et blanc :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
magick image.jpg -colorspace Gray output.jpg
|
||||||
|
```
|
||||||
|
|
||||||
|
Réglage de la luminosité et du contraste (valeurs en pourcentage, positives ou négatives) :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
magick image.jpg -brightness-contrast 10x5 output.jpg
|
||||||
|
```
|
||||||
|
|
||||||
|
Ici, +10% de luminosité et +5% de contraste. Pour assombrir, on utilise des valeurs négatives : `-brightness-contrast -10x0`.
|
||||||
|
|
||||||
|
## 11. Inspecter une image
|
||||||
|
|
||||||
|
Pour obtenir les informations essentielles — format, dimensions, profondeur :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
magick identify image.jpg
|
||||||
|
```
|
||||||
|
|
||||||
|
Pour tout savoir, y compris les métadonnées EXIF, le profil colorimétrique, l'histogramme :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
magick identify -verbose image.jpg
|
||||||
|
```
|
||||||
|
|
||||||
|
La sortie verbeuse peut faire plusieurs pages, mais c'est inestimable pour diagnostiquer un problème ou comprendre d'où vient un fichier.
|
||||||
|
|
||||||
|
## 12. Formats modernes
|
||||||
|
|
||||||
|
Le WebP de Google offre une compression nettement meilleure que le JPEG à qualité équivalente, et il est aujourd'hui supporté par tous les navigateurs courants :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
magick image.jpg -quality 80 image.webp
|
||||||
|
```
|
||||||
|
|
||||||
|
L'AVIF va encore plus loin en termes de compression, au prix d'un encodage plus lent :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
magick image.jpg image.avif
|
||||||
|
```
|
||||||
|
|
||||||
|
Si la commande échoue avec une erreur de délégué, c'est que votre installation d'ImageMagick a été compilée sans le support AVIF — il faudra installer `libheif` ou recompiler.
|
||||||
|
|
||||||
|
## 13. Quelques règles à se fixer
|
||||||
|
|
||||||
|
- **Toujours travailler sur une copie** quand on découvre une nouvelle commande. `mogrify` en particulier ne pardonne pas.
|
||||||
|
- **Stripper les métadonnées** avant toute publication web.
|
||||||
|
- **Pour de très gros volumes** (plusieurs milliers d'images, ou des images très lourdes), regarder du côté de `libvips` : c'est plus rapide et beaucoup moins gourmand en mémoire qu'ImageMagick. Pour tout le reste, ImageMagick est largement suffisant.
|
||||||
|
- **Automatiser dès qu'on répète** : si la même commande revient deux fois, elle mérite un script.
|
||||||
|
- **Lire les messages d'erreur** : ImageMagick est verbeux, et la plupart des problèmes (délégué manquant, permissions, format non reconnu) sont explicitement nommés dans la sortie.
|
||||||
|
|
||||||
|
## 14. Là où on le croise vraiment
|
||||||
|
|
||||||
|
En pratique, ImageMagick finit presque toujours dans les mêmes situations :
|
||||||
|
|
||||||
|
- préparation d'images pour un site web (redimensionnement + compression + strip),
|
||||||
|
- génération de miniatures à la volée côté serveur,
|
||||||
|
- normalisation d'un catalogue photo hétérogène (formats, tailles, profils),
|
||||||
|
- conversion massive d'archives anciennes vers des formats modernes,
|
||||||
|
- nettoyage des métadonnées avant diffusion publique.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
ImageMagick fait partie de ces outils qu'on apprivoise lentement mais qu'on garde longtemps. Au début, on copie des commandes trouvées en ligne sans tout comprendre. Puis on commence à reconnaître les options, à les combiner, à écrire ses propres scripts. Et un jour, on se rend compte qu'on a remplacé un logiciel entier par trois lignes de Bash — et qu'on n'a jamais été aussi efficace pour traiter des images.
|
||||||
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 141 KiB |
|
After Width: | Height: | Size: 67 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 86 KiB |
@@ -1,204 +1,87 @@
|
|||||||
## Comment les réseaux mobiles évitent la saturation
|
Un attentat, un séisme, un match du Stade de France, une grande panne d'électricité. Dans ces moments-là, des centaines de milliers de gens dégainent leur téléphone au même instant. Le réseau mobile est dimensionné pour un usage moyen, pas pour un pic massif simultané, et il devrait théoriquement s'effondrer. La plupart du temps, il tient. Pas parfaitement, pas pour tout le monde, mais il tient — et surtout, les appels d'urgence continuent de passer. C'est le résultat d'une série de mécanismes empilés depuis les années 1990, que la 4G a affinés et que la 5G a élargis. Cet article les passe en revue, et termine sur une question qu'on me pose souvent : est-ce que mon forfait à 50 € me donne une place prioritaire dans cette file d'attente ?
|
||||||
|
|
||||||
Quand beaucoup de personnes utilisent leur téléphone en même temps (catastrophe, événement sportif, panne électrique, etc.), le réseau mobile peut rapidement être saturé.
|
## Trois questions, pas une
|
||||||
Depuis la 2G, les opérateurs ont mis en place des mécanismes pour :
|
|
||||||
|
|
||||||
* éviter l’effondrement total du réseau,
|
Quand une cellule commence à chauffer, l'opérateur doit répondre à trois questions distinctes. Qui a le droit de se connecter ? Une fois connecté, qui passe en premier ? Et quels services doivent absolument continuer à fonctionner, quoi qu'il arrive ?
|
||||||
* garantir l’accès aux **services essentiels**,
|
|
||||||
* maintenir les **communications d’urgence**.
|
|
||||||
|
|
||||||
En 4G et en 5G, ces mécanismes existent toujours, mais sous des formes **beaucoup plus sophistiquées**.
|
La 2G ne savait répondre qu'à la première. Elle filtrait à l'entrée et basta. La 4G a ajouté la deuxième : une fois admis sur le réseau, votre trafic est traité différemment selon son importance. La 5G ajoute la troisième : elle peut créer des réseaux virtuels parallèles dont certains sont réservés à des usages critiques, totalement isolés des autres.
|
||||||
|
|
||||||
|
## Le filtrage à l'entrée
|
||||||
|
|
||||||
|
Chaque carte SIM porte un numéro de classe d'accès, hérité du GSM, entre 0 et 15. Les classes 0 à 9 couvrent le grand public — autrement dit nous tous. Les classes 11 à 15 sont réservées : services de secours, autorités publiques, personnel opérateur, usages militaires selon les pays.
|
||||||
|
|
||||||
|
Quand une cellule est surchargée, l'eNodeB (la station de base 4G) diffuse une consigne aux téléphones du secteur : « les classes 0 à 9, vous attendez ». C'est l'**Access Class Barring**. Concrètement, votre téléphone reçoit ce message et bloque lui-même votre tentative d'appel ou de connexion data, sans même envoyer la demande à la station. C'est élégant parce que ça soulage la station avant même qu'elle ne soit sollicitée. Les classes prioritaires, elles, passent sans encombre.
|
||||||
|
|
||||||
|
Une variante plus dure, l'**Extended Access Barring**, vise les objets connectés et les usages non urgents. Quand une vraie crise se déclare, l'opérateur peut couper les compteurs intelligents, les alarmes domestiques et autres équipements bavards pour préserver la bande passante humaine.
|
||||||
|
|
||||||
|
En 5G, ce mécanisme a été refondu sous le nom d'**UAC** — *Unified Access Control*, introduit dans la Release 15 du 3GPP. UAC unifie dans un seul cadre ce qui était auparavant éparpillé entre ACB, EAB et d'autres dispositifs spécifiques. Il repose sur deux notions complémentaires. Les *Access Identities* identifient qui vous êtes : utilisateur lambda, abonné à un service prioritaire type MPS ou MCS, personnel d'urgence, agent opérateur. Les *Access Categories* identifient ce que vous essayez de faire : appel d'urgence, connexion data normale, SMS, mise à jour de localisation. La combinaison des deux détermine si votre demande passe ou pas. La granularité gagnée par rapport à la 4G est réelle : on peut bloquer un type d'action précis pour un type d'utilisateur précis, par exemple « les abonnés grand public ne peuvent plus initier de nouveaux appels data, mais les SMS et les appels voix continuent ».
|
||||||
|
|
||||||
|
## La priorité une fois connecté
|
||||||
|
|
||||||
|
Là où la 4G a vraiment innové, c'est en introduisant le **QCI** — *QoS Class Identifier*. Chaque flux de données qui transite sur le réseau se voit attribuer un numéro entre 1 et 9 (avec quelques valeurs supplémentaires pour des cas spéciaux) qui dit à l'infrastructure comment le traiter.
|
||||||
|
|
||||||
|
| Usage | QCI | Traitement |
|
||||||
|
|---|---|---|
|
||||||
|
| Appel VoLTE (voix sur LTE) | 1 | Latence minimale, débit garanti |
|
||||||
|
| Visioconférence | 2 | Débit garanti |
|
||||||
|
| Signalisation réseau | 5 | Très haute priorité |
|
||||||
|
| Streaming vidéo | 6 ou 8 | Best effort prioritaire |
|
||||||
|
| Web et internet général | 9 | Best effort standard |
|
||||||
|
|
||||||
|
Quand la cellule est encombrée, le routeur sait quoi sacrifier en premier. YouTube va ralentir, les pages web vont mettre du temps à charger, mais l'appel téléphonique de votre voisin reste audible. C'est un compromis assumé : on dégrade volontairement les usages secondaires pour préserver les usages critiques.
|
||||||
|
|
||||||
|
La 5G a transposé ce mécanisme sous le nom de **5QI** (*5G QoS Identifier*) avec davantage de niveaux et une meilleure prise en compte des cas que la 4G gérait mal — notamment les services à très basse latence pour les usines connectées ou la voiture autonome. La voix d'urgence garde son sommet, les données critiques industrielles s'intercalent juste après, le streaming et le web restent en bas de la pile.
|
||||||
|
|
||||||
|
## L'isolation par tranches : le network slicing
|
||||||
|
|
||||||
|
C'est l'apport majeur de la 5G en matière de gestion de crise. Au lieu de partager une seule infrastructure entre tous les usages, on peut maintenant la découper logiciellement en tranches — des *slices* — qui se comportent comme autant de réseaux indépendants, alors qu'ils tournent sur les mêmes antennes et les mêmes câbles.
|
||||||
|
|
||||||
|
Un opérateur peut par exemple maintenir une tranche pour le grand public avec ses millions d'abonnés et son trafic massif, une autre pour les services d'urgence dimensionnée pour rester fluide même quand le reste sature, une troisième pour les objets connectés industriels avec des garanties de latence, et une quatrième pour des opérateurs critiques type SNCF, EDF ou hôpitaux. Chaque tranche a ses propres règles d'admission, ses propres priorités, ses propres garanties de performance. Si la tranche grand public est totalement saturée, celle des secours ne le sait même pas.
|
||||||
|
|
||||||
|
Cette isolation est ce qui distingue le plus fondamentalement la 5G des générations précédentes. Avant, tout le monde se battait pour les mêmes ressources, avec juste des priorités différentes pour départager. Maintenant, certaines ressources sont retirées du combat dès le départ.
|
||||||
|
|
||||||
|
## Récapitulatif
|
||||||
|
|
||||||
|
| Génération | Ce qui est contrôlé | Comment |
|
||||||
|
|---|---|---|
|
||||||
|
| 2G | L'accès au réseau | Classes d'accès 0-15 |
|
||||||
|
| 4G | L'accès + la priorité du trafic | ACB / EAB + QCI |
|
||||||
|
| 5G | L'accès + la priorité + l'isolation des services | UAC + 5QI + network slicing |
|
||||||
|
|
||||||
|
Tous ces mécanismes restent invisibles tant que tout va bien. Vous ne savez pas qu'ils existent. Vous découvrez leur existence le jour où votre voisin n'arrive plus à charger ses mails alors que les pompiers, eux, continuent de communiquer normalement. Ce jour-là, ce n'est pas de la magie. C'est trente ans d'ingénierie radio qui ont anticipé que ça arriverait.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# 1. Le principe général
|
## Et mon forfait premium, alors ?
|
||||||
|
|
||||||
Dans un réseau moderne, il y a **trois niveaux de contrôle** :
|
Question logique à ce stade. Si le réseau sait techniquement prioriser certains flux par rapport à d'autres, qu'est-ce qui empêche un opérateur de faire passer ses abonnés à 50 € devant ceux à 10 € quand les antennes saturent ? La réponse honnête commence par un aveu : techniquement, rien. L'outil existe, il s'appelle **Quality of Service** (QoS), c'est exactement le mécanisme qu'on vient de décrire. Si demain Orange ou SFR voulaient créer une voie rapide pour leurs abonnés haut de gamme, ils auraient les outils dans la boîte. Pourtant, ils ne le font pas. Pour quatre raisons.
|
||||||
|
|
||||||
1. **Qui a le droit d’entrer sur le réseau ?**
|
### La loi européenne l'interdit
|
||||||
2. **Qui est prioritaire une fois connecté ?**
|
|
||||||
3. **Quels services doivent absolument continuer à fonctionner ?**
|
|
||||||
|
|
||||||
La 2G répondait surtout à la première question.
|
Le règlement **(UE) 2015/2120**, dit « règlement internet ouvert », oblige les opérateurs à traiter tout le trafic de la même façon, sans discrimination liée à l'expéditeur, au destinataire, au contenu ou à l'application. Il a fêté ses dix ans en novembre 2025, et l'ARCEP a profité de l'anniversaire pour rappeler que c'est l'un des piliers du modèle numérique européen. Les sanctions sont sérieuses : jusqu'à **3 % du chiffre d'affaires** de l'opérateur fautif. Un opérateur français qui annoncerait demain « avec notre forfait Premium, vous passez devant les autres » se retrouverait devant l'ARCEP dans la semaine.
|
||||||
La 4G et la 5G répondent **aux trois**.
|
|
||||||
|
|
||||||
---
|
Le règlement laisse quelques portes ouvertes pour les services dits « spécialisés » qui ont besoin d'une qualité garantie — téléchirurgie, voiture connectée. Mais ces exceptions sont étroitement encadrées et ne couvrent absolument pas le confort d'un client haut de gamme qui voudrait charger son Instagram plus vite à 19h.
|
||||||
|
|
||||||
# 2. En 4G (LTE)
|
Aux États-Unis, l'histoire est différente. La FCC a tenté de restaurer la neutralité du net en 2024, mais en janvier 2025 la cour d'appel du sixième circuit a invalidé la décision, jugeant que la FCC n'avait pas l'autorité légale pour reclasser le haut débit comme service public. Avec l'arrivée de Brendan Carr à la tête de la FCC, ouvertement opposé à la neutralité du net, il n'y a aujourd'hui plus de règle fédérale outre-Atlantique. Quelques États (Californie, Washington, New York, Oregon) ont leurs propres lois qui maintiennent le principe, mais à l'échelle du pays, les opérateurs américains pourraient légalement faire ce que leurs homologues européens n'ont pas le droit de faire. Pourtant, ils ne le font pas ouvertement non plus, et la raison renvoie aux trois points suivants.
|
||||||
|
|
||||||
## 2.1 Contrôle d’accès : Access Class Barring
|
### C'est commercialement intenable
|
||||||
|
|
||||||
Chaque carte SIM contient toujours une **classe d’accès** (0 à 15).
|
Imagine la publicité : « Forfait Premium à 50 € — passez devant les pauvres pendant les heures de pointe ». Le slogan ne se vend pas. Les directions marketing savent que dire à la moitié de leurs clients qu'ils sont des citoyens de seconde zone du réseau est le plus court chemin vers une crise de réputation. C'est pour ça qu'on vous vend « plus de Go », « 5G ultra rapide », « roaming inclus dans 110 pays » — des promesses qui sonnent positivement sans jamais dire à personne qu'il est désavantagé.
|
||||||
|
|
||||||
* Classes **0 à 9** : grand public
|
### L'effet boule de neige serait toxique
|
||||||
* Classes **11 à 15** : sécurité, urgence, opérateur, etc.
|
|
||||||
|
|
||||||
En cas de surcharge, le réseau peut activer l’**Access Class Barring** :
|
Imagine que ça se mette quand même en place. Les riches passent devant. Les antennes restent saturées pour les autres, qui se mettent à payer plus pour échapper à la saturation, ce qui sature encore plus les bas forfaits, ce qui pousse encore plus de gens à monter en gamme. Au bout de cinq ans, on a un réseau à deux vitesses où les forfaits modestes deviennent quasi inutilisables aux heures critiques, et où la connexion mobile correcte devient un service de luxe. Ce n'est plus un service de télécommunications, c'est un système de classes.
|
||||||
|
|
||||||
* certaines classes sont **bloquées temporairement**,
|
C'est exactement ce que la neutralité du net cherche à empêcher. Pas par idéologie, mais parce qu'on a déjà vu où mène ce genre de spirale dans les pays où elle n'est pas protégée. Certains opérateurs proposent par exemple des forfaits où Facebook et WhatsApp sont gratuits mais où le reste est payant, ce qui revient à dire que le bon internet est celui que l'opérateur a choisi pour vous. Ce n'est plus tout à fait le même service.
|
||||||
* les classes prioritaires restent **autorisées**.
|
|
||||||
|
|
||||||
👉 C’est l’héritier direct des classes GSM.
|
### Ça ne résoudrait rien
|
||||||
|
|
||||||
---
|
Quand un réseau sature, ce n'est pas un problème de répartition entre utilisateurs, c'est un problème de **capacité totale**. Faire passer Pierre avant Paul ne crée pas un seul bit de bande passante supplémentaire. Ça déplace juste le problème de l'un vers l'autre. La vraie solution, quand une cellule sature trop souvent, c'est d'installer plus d'antennes, de densifier le réseau, de basculer sur une fréquence plus performante ou de passer à la génération suivante. C'est cher, c'est long, ça implique des autorisations administratives et des négociations foncières, mais c'est la seule réponse qui tient la route. Prioriser, c'est rapide, mais ça repousse le mur, ça ne le déplace pas.
|
||||||
|
|
||||||
## 2.2 Priorité une fois connecté : la QoS
|
C'est comme si on proposait une voie réservée aux Mercedes sur l'A7 un samedi de chassé-croisé. Techniquement, on peut peindre la ligne au sol et installer les panneaux dans la matinée. Mais cette voie ne réduit pas le bouchon, elle le concentre sur les voies restantes ; elle écorne le principe d'égalité d'accès à l'infrastructure publique ; et elle ne change rien au problème de fond, qui est qu'il y a trop de voitures pour la route disponible. La vraie solution reste la même qu'avant : élargir l'autoroute, ou convaincre une partie des gens de prendre le train.
|
||||||
|
|
||||||
En 4G, être connecté ne suffit pas.
|
### Le caveat 5G
|
||||||
Chaque communication reçoit un **niveau de qualité de service** appelé **QCI** (*QoS Class Identifier*).
|
|
||||||
|
|
||||||
On peut comparer cela à des **voies de circulation** :
|
Une nuance honnête pour finir. Le *network slicing* complique le débat juridique. Un opérateur peut créer des tranches de réseau avec des qualités différenciées en toute légalité quand il s'agit d'usages spécialisés — santé, industrie, transports. La question qui agite régulateurs et juristes depuis plusieurs années est de savoir où finit le service spécialisé légitime et où commence le contournement déguisé de la neutralité du net. L'ARCEP a ouvert ce chantier, et c'est probablement là, plus que dans une revanche commerciale brutale sur les forfaits premium, que se jouera la prochaine bataille.
|
||||||
|
|
||||||
* autoroute prioritaire pour les urgences,
|
Mais pour répondre simplement à la question : non, votre forfait à 50 € ne vous donne pas la priorité réseau sur celui de votre voisin à 10 €. Il vous donne plus de data, parfois un meilleur débit théorique, des options en plus. Pas une place dans la file.
|
||||||
* route normale pour le trafic courant.
|
|
||||||
|
|
||||||
### Exemples simples
|
|
||||||
|
|
||||||
| Usage | QCI | Priorité |
|
|
||||||
| ------------------------- | --- | ---------- |
|
|
||||||
| Appels VoLTE | 1 | Très haute |
|
|
||||||
| Signalisation réseau | 5 | Haute |
|
|
||||||
| Internet mobile classique | 9 | Normale |
|
|
||||||
|
|
||||||
Résultat :
|
|
||||||
|
|
||||||
* même si la data ralentit,
|
|
||||||
* les **appels d’urgence** et la **signalisation** passent toujours en premier.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2.3 Ce que la 4G a changé
|
|
||||||
|
|
||||||
Par rapport au GSM :
|
|
||||||
|
|
||||||
* on ne gère plus seulement **l’accès**,
|
|
||||||
* on gère aussi **la qualité et la priorité du trafic**.
|
|
||||||
|
|
||||||
Le réseau peut :
|
|
||||||
|
|
||||||
* ralentir YouTube,
|
|
||||||
* mais maintenir la voix et les appels critiques.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# 3. En 5G
|
|
||||||
|
|
||||||
La 5G reprend tout ce qui existe en 4G **et ajoute une couche stratégique**.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3.1 Contrôle d’accès : toujours présent
|
|
||||||
|
|
||||||
On retrouve :
|
|
||||||
|
|
||||||
* Access Class Barring,
|
|
||||||
* Extended Access Barring pour les situations de crise.
|
|
||||||
|
|
||||||
Les classes prioritaires (secours, autorités, opérateurs) conservent leur accès même quand le réseau est saturé.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3.2 Priorité du trafic : 5QI
|
|
||||||
|
|
||||||
Le QCI devient **5QI** (*5G QoS Identifier*).
|
|
||||||
|
|
||||||
Même principe, mais plus précis :
|
|
||||||
|
|
||||||
* plus de niveaux,
|
|
||||||
* meilleure adaptation au service.
|
|
||||||
|
|
||||||
### Exemples
|
|
||||||
|
|
||||||
| Service | 5QI | Objectif |
|
|
||||||
| ----------------- | --- | ---------------- |
|
|
||||||
| Voix d’urgence | 1 | Latence minimale |
|
|
||||||
| Données critiques | 7 | Fiabilité élevée |
|
|
||||||
| Internet standard | 9 | Best effort |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3.3 La grande nouveauté : le Network Slicing
|
|
||||||
|
|
||||||
La 5G introduit un concept clé : **le découpage du réseau en tranches logiques**.
|
|
||||||
|
|
||||||
On peut comparer cela à plusieurs **réseaux virtuels** sur une même infrastructure physique.
|
|
||||||
|
|
||||||
### Exemples de slices
|
|
||||||
|
|
||||||
* Slice **grand public**
|
|
||||||
* Slice **services d’urgence**
|
|
||||||
* Slice **transports / énergie**
|
|
||||||
* Slice **objets connectés**
|
|
||||||
|
|
||||||
Chaque slice a :
|
|
||||||
|
|
||||||
* ses propres règles de priorité,
|
|
||||||
* ses propres garanties de performance,
|
|
||||||
* son propre contrôle d’accès.
|
|
||||||
|
|
||||||
👉 Si le slice grand public est saturé,
|
|
||||||
le slice des secours **continue de fonctionner normalement**.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# 4. Ce que cela change concrètement
|
|
||||||
|
|
||||||
## En situation normale
|
|
||||||
|
|
||||||
Tout le monde accède au réseau sans différence visible.
|
|
||||||
|
|
||||||
## En situation de crise
|
|
||||||
|
|
||||||
Le réseau applique automatiquement :
|
|
||||||
|
|
||||||
1. **Filtrage à l’entrée**
|
|
||||||
|
|
||||||
* certaines cartes SIM peuvent être temporairement bloquées.
|
|
||||||
2. **Priorité du trafic**
|
|
||||||
|
|
||||||
* appels et services critiques passent avant le reste.
|
|
||||||
3. **Isolation par slices (5G)**
|
|
||||||
|
|
||||||
* les services vitaux sont protégés de la saturation générale.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# 5. Et les forfaits dans tout ça ?
|
|
||||||
|
|
||||||
Une question fréquente :
|
|
||||||
|
|
||||||
> Les forfaits “premium”, “illimités” donnent-ils une priorité réseau ?
|
|
||||||
|
|
||||||
**Réponse : non.**
|
|
||||||
|
|
||||||
Les priorités techniques ne dépendent **pas** :
|
|
||||||
|
|
||||||
* du prix du forfait,
|
|
||||||
* des options commerciales.
|
|
||||||
|
|
||||||
Elles dépendent uniquement :
|
|
||||||
|
|
||||||
* du **profil réseau** associé à la SIM,
|
|
||||||
* du **statut de l’utilisateur** (grand public, sécurité, opérateur, urgence),
|
|
||||||
* des **politiques de gestion de crise** de l’opérateur.
|
|
||||||
|
|
||||||
Un abonné classique, même avec un forfait haut de gamme, reste dans une **classe standard**.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# 6. Comparaison simple
|
|
||||||
|
|
||||||
| Génération | Ce qui est contrôlé | Comment |
|
|
||||||
| ---------- | ---------------------------- | ------------------- |
|
|
||||||
| **2G** | Accès au réseau | Classes d’accès |
|
|
||||||
| **4G** | Accès + priorité trafic | ACB + QCI |
|
|
||||||
| **5G** | Accès + priorité + isolation | ACB + 5QI + slicing |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# 7. En résumé
|
|
||||||
|
|
||||||
* Les **classes d’accès** du GSM existent toujours en 4G et 5G sous forme modernisée.
|
|
||||||
* La **4G** ajoute la gestion fine de la priorité du trafic.
|
|
||||||
* La **5G** va plus loin avec le **network slicing**, qui permet de garantir des réseaux quasi indépendants pour les services essentiels.
|
|
||||||
* Les **forfaits commerciaux** n’influencent pas ces mécanismes : seule la **politique réseau** de l’opérateur compte.
|
|
||||||
@@ -1,13 +1,162 @@
|
|||||||
{
|
{
|
||||||
"uuid": "4f443bcb-b0d4-47f8-837d-61627e6c94f2",
|
"uuid": "4f443bcb-b0d4-47f8-837d-61627e6c94f2",
|
||||||
"slug": "priorites-et-acces-au-reseau-en-4g-et-5g",
|
"slug": "priorites-et-acces-au-reseau-en-4g-et-5g",
|
||||||
"title": "Priorités et accès au réseau en 4G et 5G",
|
"title": "Pourquoi le réseau mobile ne s'effondre pas le jour où tout le monde téléphone en même temps",
|
||||||
"author": "cedric@abonnel.fr",
|
"author": "cedric@abonnel.fr",
|
||||||
"published": true,
|
"published": true,
|
||||||
"published_at": "2026-01-06 22:21:04",
|
"published_at": "2026-01-06 22:21",
|
||||||
"created_at": "2026-01-06 22:21:04",
|
"created_at": "2026-01-06 22:21:04",
|
||||||
"updated_at": "2026-01-06 22:21:04",
|
"updated_at": "2026-05-11 23:40:18",
|
||||||
"revisions": [],
|
"revisions": [
|
||||||
|
{
|
||||||
|
"n": 1,
|
||||||
|
"date": "2026-05-11 23:06:50",
|
||||||
|
"comment": "",
|
||||||
|
"title": "Comment les réseaux mobiles tiennent debout quand tout le monde téléphone en même temps"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"n": 2,
|
||||||
|
"date": "2026-05-11 23:09:07",
|
||||||
|
"comment": "",
|
||||||
|
"title": "Comment les réseaux mobiles tiennent debout quand tout le monde téléphone en même temps"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"n": 3,
|
||||||
|
"date": "2026-05-11 23:11:33",
|
||||||
|
"comment": "",
|
||||||
|
"title": "Comment les réseaux mobiles tiennent debout quand tout le monde téléphone en même temps"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"n": 4,
|
||||||
|
"date": "2026-05-11 23:14:26",
|
||||||
|
"comment": "",
|
||||||
|
"title": "Comment les réseaux mobiles tiennent debout quand tout le monde téléphone en même temps"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"n": 5,
|
||||||
|
"date": "2026-05-11 23:16:33",
|
||||||
|
"comment": "",
|
||||||
|
"title": "Comment les réseaux mobiles tiennent debout quand tout le monde téléphone en même temps"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"n": 6,
|
||||||
|
"date": "2026-05-11 23:16:56",
|
||||||
|
"comment": "",
|
||||||
|
"title": "Comment les réseaux mobiles tiennent debout quand tout le monde téléphone en même temps"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"n": 7,
|
||||||
|
"date": "2026-05-11 23:40:18",
|
||||||
|
"comment": "",
|
||||||
|
"title": "Pourquoi le réseau mobile ne s'effondre pas le jour où tout le monde téléphone en même temps"
|
||||||
|
}
|
||||||
|
],
|
||||||
"cover": "cover.jpg",
|
"cover": "cover.jpg",
|
||||||
|
"files_meta": {
|
||||||
|
"_preview.png": {
|
||||||
|
"author": "",
|
||||||
|
"source_url": ""
|
||||||
|
},
|
||||||
|
"_thumb_dc5413a86e9c042a-22419.jpg": {
|
||||||
|
"author": "",
|
||||||
|
"source_url": ""
|
||||||
|
},
|
||||||
|
"_thumb_945482b26e8a76ab-49498.png": {
|
||||||
|
"author": "",
|
||||||
|
"source_url": ""
|
||||||
|
},
|
||||||
|
"_thumb_a1080cd703289e6b-144512.png": {
|
||||||
|
"author": "",
|
||||||
|
"source_url": ""
|
||||||
|
},
|
||||||
|
"_thumb_cae639a42d79414f-68314.jpg": {
|
||||||
|
"author": "",
|
||||||
|
"source_url": ""
|
||||||
|
},
|
||||||
|
"03bcf841dafcc9f4-87771.jpg": {
|
||||||
|
"author": "",
|
||||||
|
"source_url": "https://www.ariase.com/uploads/media/124b4e1790cf20b8fed653884ef1eb46d235922f.jpeg"
|
||||||
|
},
|
||||||
|
"cover.jpg": {
|
||||||
|
"author": "",
|
||||||
|
"source_url": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"external_links": [
|
||||||
|
{
|
||||||
|
"url": "https://arxiv.org/pdf/2012.05520",
|
||||||
|
"name": "An Overview of 5G System Accessibility Differentiation and Control",
|
||||||
|
"added_at": "2026-05-11 23:04:53",
|
||||||
|
"author": "-",
|
||||||
|
"meta": {
|
||||||
|
"mime": "application/pdf",
|
||||||
|
"size": 717772,
|
||||||
|
"description": "IEEE Transactions on Magnetics",
|
||||||
|
"date": "2021-09-28 10:31:29+02:00",
|
||||||
|
"subject": "IEEE Transactions on Magnetics",
|
||||||
|
"creator": "-",
|
||||||
|
"producer": "Microsoft® Word for Microsoft 365",
|
||||||
|
"pages": 9,
|
||||||
|
"pdf_version": "PDF 1.7",
|
||||||
|
"page_size": "Letter (216×279 mm)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://www.techplayon.com/5g-nr-cell-access-control/",
|
||||||
|
"name": "5G NR Cell Access Control - Techplayon - RRC Signalling",
|
||||||
|
"added_at": "2026-05-11 23:05:34",
|
||||||
|
"author": "Author",
|
||||||
|
"meta": {
|
||||||
|
"mime": "text/html",
|
||||||
|
"size": 79963,
|
||||||
|
"description": "Access Control barring refers to a traffic congestion control mechanism to secure and ensure the success of critical communications",
|
||||||
|
"og_image": "/file?uuid=4f443bcb-b0d4-47f8-837d-61627e6c94f2&name=_thumb_dc5413a86e9c042a-22419.jpg",
|
||||||
|
"site_name": "Techplayon",
|
||||||
|
"og_type": "article",
|
||||||
|
"language": "en_US",
|
||||||
|
"date": "2019-12-21T17:44:54+00:00",
|
||||||
|
"canonical": "https://www.techplayon.com/5g-nr-cell-access-control/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://simnovus.com/unified-access-control-uac-the-gatekeeper-of-5g-networks/",
|
||||||
|
"name": "Unified Access Control (UAC): The Gatekeeper of 5G Networks - Simnovus",
|
||||||
|
"added_at": "2026-05-11 23:11:58",
|
||||||
|
"meta": {
|
||||||
|
"mime": "text/html",
|
||||||
|
"size": 124603,
|
||||||
|
"description": "Unified Access Control (UAC) is a fundamental component of 5G networks that governs access to network resources. It replaces the traditional access control",
|
||||||
|
"og_image": "/file?uuid=4f443bcb-b0d4-47f8-837d-61627e6c94f2&name=_thumb_cae639a42d79414f-68314.jpg",
|
||||||
|
"site_name": "Simnovus",
|
||||||
|
"og_type": "article",
|
||||||
|
"language": "en_US",
|
||||||
|
"date": "2024-08-14T10:58:09+05:30",
|
||||||
|
"canonical": "https://simnovus.com/unified-access-control-uac-the-gatekeeper-of-5g-networks/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://www.sharetechnote.com/html/5G/5G_UAC.html",
|
||||||
|
"name": "5G | ShareTechnote",
|
||||||
|
"added_at": "2026-05-11 23:12:20",
|
||||||
|
"meta": {
|
||||||
|
"mime": "text/html",
|
||||||
|
"size": 28707,
|
||||||
|
"og_image": "/file?uuid=4f443bcb-b0d4-47f8-837d-61627e6c94f2&name=_thumb_a1080cd703289e6b-144512.png"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://www.telecomgurukul.com/post/advanced-access-control-mechanisms-in-lte-and-5g-nr-networks",
|
||||||
|
"name": "Advanced Access Control Mechanisms in LTE and 5G NR Networks",
|
||||||
|
"added_at": "2026-05-11 23:14:17",
|
||||||
|
"meta": {
|
||||||
|
"mime": "text/html",
|
||||||
|
"size": 1344890,
|
||||||
|
"og_image": "/file?uuid=4f443bcb-b0d4-47f8-837d-61627e6c94f2&name=_thumb_945482b26e8a76ab-49498.png"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"seo_title": "",
|
||||||
|
"seo_description": "",
|
||||||
|
"og_image": "",
|
||||||
"category": "télécom"
|
"category": "télécom"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
Un attentat, un séisme, un match du Stade de France, une grande panne d'électricité. Dans ces moments-là, des centaines de milliers de gens dégainent leur téléphone en même temps. Le réseau mobile, qui est dimensionné pour un usage moyen et pas pour un pic massif simultané, devrait théoriquement s'effondrer. La plupart du temps, il tient. Pas parfaitement, pas pour tout le monde, mais il tient — et surtout, les appels d'urgence continuent de passer. C'est le résultat d'une série de mécanismes empilés depuis les années 1990, et que la 4G et la 5G ont raffinés. Voici comment ça marche, sans le jargon mais sans non plus mentir sur ce qui se passe vraiment.
|
||||||
|
|
||||||
|
## Trois questions, pas une
|
||||||
|
|
||||||
|
Dans un réseau cellulaire moderne, l'opérateur doit répondre à trois questions distinctes quand la cellule commence à chauffer. Qui a le droit de se connecter ? Une fois connecté, qui passe en premier ? Et quels services doivent absolument continuer à fonctionner, quoi qu'il arrive ?
|
||||||
|
|
||||||
|
La 2G ne savait répondre qu'à la première. Elle filtrait à l'entrée et basta. La 4G a ajouté la deuxième : une fois admis sur le réseau, votre trafic n'est plus traité de la même manière selon son importance. La 5G ajoute la troisième : elle peut littéralement créer des réseaux virtuels parallèles, dont certains sont réservés à des usages critiques et isolés des autres.
|
||||||
|
|
||||||
|
## En 4G : filtrer puis prioriser
|
||||||
|
|
||||||
|
### Filtrer à l'entrée
|
||||||
|
|
||||||
|
Chaque carte SIM porte un numéro de classe d'accès, hérité du GSM, entre 0 et 15. Les classes 0 à 9 couvrent le grand public — autrement dit nous tous. Les classes 11 à 15 sont réservées : services de secours, autorités publiques, personnel opérateur, usages militaires selon les pays.
|
||||||
|
|
||||||
|
Quand une cellule est surchargée, l'eNodeB (la station de base 4G) diffuse une consigne aux téléphones du secteur : « les classes 0 à 9, vous attendez ». C'est l'**Access Class Barring**. Concrètement, votre téléphone reçoit ce message et bloque lui-même votre tentative d'appel ou de connexion data, sans même envoyer la demande à la station. C'est élégant parce que ça soulage la station avant même qu'elle ne soit sollicitée. Les classes prioritaires, elles, passent sans encombre.
|
||||||
|
|
||||||
|
Il existe une variante plus dure appelée **Extended Access Barring**, conçue pour les objets connectés et les usages non urgents. Quand une vraie crise se déclare, l'opérateur peut couper les compteurs intelligents, les alarmes domestiques et autres équipements bavards pour préserver la bande passante humaine.
|
||||||
|
|
||||||
|
### Prioriser une fois connecté
|
||||||
|
|
||||||
|
Là où la 4G a vraiment innové, c'est en introduisant le **QCI** — *QoS Class Identifier*. Chaque flux de données qui transite sur le réseau se voit attribuer un numéro entre 1 et 9 (et quelques valeurs au-dessus pour des cas spéciaux) qui dit à l'infrastructure comment le traiter.
|
||||||
|
|
||||||
|
Quelques exemples concrets :
|
||||||
|
|
||||||
|
| Usage | QCI | Traitement |
|
||||||
|
|---|---|---|
|
||||||
|
| Appel VoLTE (voix sur LTE) | 1 | Latence minimale, débit garanti |
|
||||||
|
| Signalisation réseau | 5 | Très haute priorité |
|
||||||
|
| Visioconférence | 2 | Débit garanti |
|
||||||
|
| Streaming vidéo | 6 ou 8 | Best effort prioritaire |
|
||||||
|
| Web et internet général | 9 | Best effort standard |
|
||||||
|
|
||||||
|
Quand la cellule est encombrée, le routeur sait quoi sacrifier en premier. YouTube va ralentir, les pages web vont mettre du temps à charger, mais l'appel téléphonique de votre voisin reste audible. C'est un compromis assumé : on dégrade volontairement les usages secondaires pour préserver les usages critiques.
|
||||||
|
|
||||||
|
## En 5G : ajouter le découpage
|
||||||
|
|
||||||
|
### Un mécanisme d'accès refondu
|
||||||
|
|
||||||
|
La 5G garde l'esprit du barring mais change son nom et sa mécanique. L'ancien Access Class Barring est remplacé par l'**UAC** — *Unified Access Control*, introduit dans la Release 15 du 3GPP. L'idée est d'unifier dans un seul cadre ce qui était auparavant éparpillé entre ACB, EAB et d'autres mécanismes spécifiques.
|
||||||
|
|
||||||
|
UAC repose sur deux notions. Les **Access Identities** identifient qui vous êtes (utilisateur lambda, abonné à un service prioritaire type MPS ou MCS, personnel d'urgence, agent opérateur). Les **Access Categories** identifient ce que vous essayez de faire (appel d'urgence, connexion data normale, SMS, mise à jour de localisation). La combinaison des deux détermine si votre demande passe ou pas.
|
||||||
|
|
||||||
|
Ce qui change vraiment, c'est la granularité. En 4G, on bloquait une classe entière. En 5G, on peut bloquer un type d'action précis pour un type d'utilisateur précis — par exemple « les abonnés grand public ne peuvent plus initier de nouveaux appels data, mais les SMS et les appels voix continuent ». L'opérateur peut aussi définir ses propres catégories d'accès, calées sur sa politique commerciale et technique.
|
||||||
|
|
||||||
|
### Le QCI devient le 5QI
|
||||||
|
|
||||||
|
Même logique qu'en 4G mais avec plus de finesse. Le **5QI** (*5G QoS Identifier*) propose davantage de niveaux et tient compte de cas que la 4G gérait mal, notamment les services à très basse latence pour les usines connectées ou la voiture autonome. La voix d'urgence garde son sommet, les données critiques industrielles s'intercalent juste après, le streaming et le web restent en bas de la pile.
|
||||||
|
|
||||||
|
### La vraie nouveauté : le network slicing
|
||||||
|
|
||||||
|
C'est l'apport majeur de la 5G en termes de gestion de crise. Au lieu de partager une seule infrastructure entre tous les usages, on peut maintenant la **découper logiciellement en tranches** — des *slices* — qui se comportent comme autant de réseaux indépendants, alors qu'ils tournent sur les mêmes antennes et les mêmes câbles.
|
||||||
|
|
||||||
|
Un opérateur peut par exemple maintenir :
|
||||||
|
|
||||||
|
- une tranche pour le grand public, avec ses millions d'abonnés et son trafic massif,
|
||||||
|
- une tranche pour les services d'urgence et de sécurité, dimensionnée pour rester fluide même quand le reste sature,
|
||||||
|
- une tranche pour les objets connectés industriels, avec des garanties de latence,
|
||||||
|
- une tranche pour les opérateurs critiques type SNCF, EDF, hôpitaux.
|
||||||
|
|
||||||
|
Chaque tranche a ses propres règles d'admission, ses propres priorités, ses propres garanties de performance. Si la tranche grand public est totalement saturée, celle des secours ne le sait même pas. Cette isolation est ce qui distingue le plus fondamentalement la 5G des générations précédentes, où tout le monde se battait pour les mêmes ressources, avec juste des priorités différentes.
|
||||||
|
|
||||||
|
## Et le forfait premium dans tout ça ?
|
||||||
|
|
||||||
|
Question qu'on entend souvent : si je paie un forfait à 50 € au lieu d'un forfait à 10 €, est-ce que je passe avant les autres en cas de saturation ?
|
||||||
|
|
||||||
|
Non.
|
||||||
|
|
||||||
|
Les priorités techniques décrites au-dessus ne dépendent ni du prix du forfait, ni des options commerciales souscrites. Elles dépendent du profil réseau associé à votre SIM (lui-même fonction de votre statut : grand public, secours, opérateur, services prioritaires officiels), et des politiques de gestion de crise programmées par l'opérateur. Un cadre dirigeant avec un forfait illimité reste, du point de vue du réseau, un abonné de classe d'accès 0-9 comme tout le monde.
|
||||||
|
|
||||||
|
Le forfait premium vous donne plus de data, parfois un meilleur débit théorique en conditions normales, des options de roaming, du cloud gratuit. Il ne vous donne pas la priorité face à un pompier ou à un préfet.
|
||||||
|
|
||||||
|
## Pour résumer
|
||||||
|
|
||||||
|
| Génération | Ce qui est contrôlé | Comment |
|
||||||
|
|---|---|---|
|
||||||
|
| 2G | L'accès au réseau | Classes d'accès 0-15 |
|
||||||
|
| 4G | L'accès + la priorité du trafic | ACB / EAB + QCI |
|
||||||
|
| 5G | L'accès + la priorité + l'isolation des services | UAC + 5QI + network slicing |
|
||||||
|
|
||||||
|
Ce qui est intéressant, c'est que ces mécanismes restent invisibles tant que tout va bien. Vous ne savez pas qu'ils existent. Vous découvrez leur existence le jour où votre voisin n'arrive plus à charger ses mails alors que les pompiers, eux, continuent de communiquer normalement. Ce jour-là, ce n'est pas de la magie. C'est trente ans d'ingénierie radio qui ont anticipé que ça arriverait.
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
Un attentat, un séisme, un match du Stade de France, une grande panne d'électricité. Dans ces moments-là, des centaines de milliers de gens dégainent leur téléphone en même temps. Le réseau mobile, qui est dimensionné pour un usage moyen et pas pour un pic massif simultané, devrait théoriquement s'effondrer. La plupart du temps, il tient. Pas parfaitement, pas pour tout le monde, mais il tient — et surtout, les appels d'urgence continuent de passer. C'est le résultat d'une série de mécanismes empilés depuis les années 1990, et que la 4G et la 5G ont raffinés. Voici comment ça marche, sans le jargon mais sans non plus mentir sur ce qui se passe vraiment.
|
||||||
|
|
||||||
|
## Trois questions, pas une
|
||||||
|
|
||||||
|
Dans un réseau cellulaire moderne, l'opérateur doit répondre à trois questions distinctes quand la cellule commence à chauffer. Qui a le droit de se connecter ? Une fois connecté, qui passe en premier ? Et quels services doivent absolument continuer à fonctionner, quoi qu'il arrive ?
|
||||||
|
|
||||||
|
La 2G ne savait répondre qu'à la première. Elle filtrait à l'entrée et basta. La 4G a ajouté la deuxième : une fois admis sur le réseau, votre trafic n'est plus traité de la même manière selon son importance. La 5G ajoute la troisième : elle peut littéralement créer des réseaux virtuels parallèles, dont certains sont réservés à des usages critiques et isolés des autres.
|
||||||
|
|
||||||
|
## En 4G : filtrer puis prioriser
|
||||||
|
|
||||||
|
### Filtrer à l'entrée
|
||||||
|
|
||||||
|
Chaque carte SIM porte un numéro de classe d'accès, hérité du GSM, entre 0 et 15. Les classes 0 à 9 couvrent le grand public — autrement dit nous tous. Les classes 11 à 15 sont réservées : services de secours, autorités publiques, personnel opérateur, usages militaires selon les pays.
|
||||||
|
|
||||||
|
Quand une cellule est surchargée, l'eNodeB (la station de base 4G) diffuse une consigne aux téléphones du secteur : « les classes 0 à 9, vous attendez ». C'est l'**Access Class Barring**. Concrètement, votre téléphone reçoit ce message et bloque lui-même votre tentative d'appel ou de connexion data, sans même envoyer la demande à la station. C'est élégant parce que ça soulage la station avant même qu'elle ne soit sollicitée. Les classes prioritaires, elles, passent sans encombre.
|
||||||
|
|
||||||
|
Il existe une variante plus dure appelée **Extended Access Barring**, conçue pour les objets connectés et les usages non urgents. Quand une vraie crise se déclare, l'opérateur peut couper les compteurs intelligents, les alarmes domestiques et autres équipements bavards pour préserver la bande passante humaine.
|
||||||
|
|
||||||
|
### Prioriser une fois connecté
|
||||||
|
|
||||||
|
Là où la 4G a vraiment innové, c'est en introduisant le **QCI** — *QoS Class Identifier*. Chaque flux de données qui transite sur le réseau se voit attribuer un numéro entre 1 et 9 (et quelques valeurs au-dessus pour des cas spéciaux) qui dit à l'infrastructure comment le traiter.
|
||||||
|
|
||||||
|
Quelques exemples concrets :
|
||||||
|
|
||||||
|
| Usage | QCI | Traitement |
|
||||||
|
|---|---|---|
|
||||||
|
| Appel VoLTE (voix sur LTE) | 1 | Latence minimale, débit garanti |
|
||||||
|
| Signalisation réseau | 5 | Très haute priorité |
|
||||||
|
| Visioconférence | 2 | Débit garanti |
|
||||||
|
| Streaming vidéo | 6 ou 8 | Best effort prioritaire |
|
||||||
|
| Web et internet général | 9 | Best effort standard |
|
||||||
|
|
||||||
|
Quand la cellule est encombrée, le routeur sait quoi sacrifier en premier. YouTube va ralentir, les pages web vont mettre du temps à charger, mais l'appel téléphonique de votre voisin reste audible. C'est un compromis assumé : on dégrade volontairement les usages secondaires pour préserver les usages critiques.
|
||||||
|
|
||||||
|
## En 5G : ajouter le découpage
|
||||||
|
|
||||||
|
### Un mécanisme d'accès refondu
|
||||||
|
|
||||||
|
La 5G garde l'esprit du barring mais change son nom et sa mécanique. L'ancien Access Class Barring est remplacé par l'**UAC** — *Unified Access Control*, introduit dans la Release 15 du 3GPP. L'idée est d'unifier dans un seul cadre ce qui était auparavant éparpillé entre ACB, EAB et d'autres mécanismes spécifiques.
|
||||||
|
|
||||||
|
UAC repose sur deux notions. Les **Access Identities** identifient qui vous êtes (utilisateur lambda, abonné à un service prioritaire type MPS ou MCS, personnel d'urgence, agent opérateur). Les **Access Categories** identifient ce que vous essayez de faire (appel d'urgence, connexion data normale, SMS, mise à jour de localisation). La combinaison des deux détermine si votre demande passe ou pas.
|
||||||
|
|
||||||
|
Ce qui change vraiment, c'est la granularité. En 4G, on bloquait une classe entière. En 5G, on peut bloquer un type d'action précis pour un type d'utilisateur précis — par exemple « les abonnés grand public ne peuvent plus initier de nouveaux appels data, mais les SMS et les appels voix continuent ». L'opérateur peut aussi définir ses propres catégories d'accès, calées sur sa politique commerciale et technique.
|
||||||
|
|
||||||
|
### Le QCI devient le 5QI
|
||||||
|
|
||||||
|
Même logique qu'en 4G mais avec plus de finesse. Le **5QI** (*5G QoS Identifier*) propose davantage de niveaux et tient compte de cas que la 4G gérait mal, notamment les services à très basse latence pour les usines connectées ou la voiture autonome. La voix d'urgence garde son sommet, les données critiques industrielles s'intercalent juste après, le streaming et le web restent en bas de la pile.
|
||||||
|
|
||||||
|
### La vraie nouveauté : le network slicing
|
||||||
|
|
||||||
|
C'est l'apport majeur de la 5G en termes de gestion de crise. Au lieu de partager une seule infrastructure entre tous les usages, on peut maintenant la **découper logiciellement en tranches** — des *slices* — qui se comportent comme autant de réseaux indépendants, alors qu'ils tournent sur les mêmes antennes et les mêmes câbles.
|
||||||
|
|
||||||
|
Un opérateur peut par exemple maintenir :
|
||||||
|
|
||||||
|
- une tranche pour le grand public, avec ses millions d'abonnés et son trafic massif,
|
||||||
|
- une tranche pour les services d'urgence et de sécurité, dimensionnée pour rester fluide même quand le reste sature,
|
||||||
|
- une tranche pour les objets connectés industriels, avec des garanties de latence,
|
||||||
|
- une tranche pour les opérateurs critiques type SNCF, EDF, hôpitaux.
|
||||||
|
|
||||||
|
Chaque tranche a ses propres règles d'admission, ses propres priorités, ses propres garanties de performance. Si la tranche grand public est totalement saturée, celle des secours ne le sait même pas. Cette isolation est ce qui distingue le plus fondamentalement la 5G des générations précédentes, où tout le monde se battait pour les mêmes ressources, avec juste des priorités différentes.
|
||||||
|
|
||||||
|
## Et le forfait premium dans tout ça ?
|
||||||
|
|
||||||
|
Question qu'on entend souvent : si je paie un forfait à 50 € au lieu d'un forfait à 10 €, est-ce que je passe avant les autres en cas de saturation ?
|
||||||
|
|
||||||
|
Non.
|
||||||
|
|
||||||
|
Les priorités techniques décrites au-dessus ne dépendent ni du prix du forfait, ni des options commerciales souscrites. Elles dépendent du profil réseau associé à votre SIM (lui-même fonction de votre statut : grand public, secours, opérateur, services prioritaires officiels), et des politiques de gestion de crise programmées par l'opérateur. Un cadre dirigeant avec un forfait illimité reste, du point de vue du réseau, un abonné de classe d'accès 0-9 comme tout le monde.
|
||||||
|
|
||||||
|
Le forfait premium vous donne plus de data, parfois un meilleur débit théorique en conditions normales, des options de roaming, du cloud gratuit. Il ne vous donne pas la priorité face à un pompier ou à un préfet.
|
||||||
|
|
||||||
|
## Pour résumer
|
||||||
|
|
||||||
|
| Génération | Ce qui est contrôlé | Comment |
|
||||||
|
|---|---|---|
|
||||||
|
| 2G | L'accès au réseau | Classes d'accès 0-15 |
|
||||||
|
| 4G | L'accès + la priorité du trafic | ACB / EAB + QCI |
|
||||||
|
| 5G | L'accès + la priorité + l'isolation des services | UAC + 5QI + network slicing |
|
||||||
|
|
||||||
|
Ce qui est intéressant, c'est que ces mécanismes restent invisibles tant que tout va bien. Vous ne savez pas qu'ils existent. Vous découvrez leur existence le jour où votre voisin n'arrive plus à charger ses mails alors que les pompiers, eux, continuent de communiquer normalement. Ce jour-là, ce n'est pas de la magie. C'est trente ans d'ingénierie radio qui ont anticipé que ça arriverait.
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
Un attentat, un séisme, un match du Stade de France, une grande panne d'électricité. Dans ces moments-là, des centaines de milliers de gens dégainent leur téléphone en même temps. Le réseau mobile, qui est dimensionné pour un usage moyen et pas pour un pic massif simultané, devrait théoriquement s'effondrer. La plupart du temps, il tient. Pas parfaitement, pas pour tout le monde, mais il tient — et surtout, les appels d'urgence continuent de passer. C'est le résultat d'une série de mécanismes empilés depuis les années 1990, et que la 4G et la 5G ont raffinés. Voici comment ça marche, sans le jargon mais sans non plus mentir sur ce qui se passe vraiment.
|
||||||
|
|
||||||
|
## Trois questions, pas une
|
||||||
|
|
||||||
|
Dans un réseau cellulaire moderne, l'opérateur doit répondre à trois questions distinctes quand la cellule commence à chauffer. Qui a le droit de se connecter ? Une fois connecté, qui passe en premier ? Et quels services doivent absolument continuer à fonctionner, quoi qu'il arrive ?
|
||||||
|
|
||||||
|
La 2G ne savait répondre qu'à la première. Elle filtrait à l'entrée et basta. La 4G a ajouté la deuxième : une fois admis sur le réseau, votre trafic n'est plus traité de la même manière selon son importance. La 5G ajoute la troisième : elle peut littéralement créer des réseaux virtuels parallèles, dont certains sont réservés à des usages critiques et isolés des autres.
|
||||||
|
|
||||||
|
## En 4G : filtrer puis prioriser
|
||||||
|
|
||||||
|
### Filtrer à l'entrée
|
||||||
|
|
||||||
|
Chaque carte SIM porte un numéro de classe d'accès, hérité du GSM, entre 0 et 15. Les classes 0 à 9 couvrent le grand public — autrement dit nous tous. Les classes 11 à 15 sont réservées : services de secours, autorités publiques, personnel opérateur, usages militaires selon les pays.
|
||||||
|
|
||||||
|
Quand une cellule est surchargée, l'eNodeB (la station de base 4G) diffuse une consigne aux téléphones du secteur : « les classes 0 à 9, vous attendez ». C'est l'**Access Class Barring**. Concrètement, votre téléphone reçoit ce message et bloque lui-même votre tentative d'appel ou de connexion data, sans même envoyer la demande à la station. C'est élégant parce que ça soulage la station avant même qu'elle ne soit sollicitée. Les classes prioritaires, elles, passent sans encombre.
|
||||||
|
|
||||||
|
Il existe une variante plus dure appelée **Extended Access Barring**, conçue pour les objets connectés et les usages non urgents. Quand une vraie crise se déclare, l'opérateur peut couper les compteurs intelligents, les alarmes domestiques et autres équipements bavards pour préserver la bande passante humaine.
|
||||||
|
|
||||||
|
### Prioriser une fois connecté
|
||||||
|
|
||||||
|
Là où la 4G a vraiment innové, c'est en introduisant le **QCI** — *QoS Class Identifier*. Chaque flux de données qui transite sur le réseau se voit attribuer un numéro entre 1 et 9 (et quelques valeurs au-dessus pour des cas spéciaux) qui dit à l'infrastructure comment le traiter.
|
||||||
|
|
||||||
|
Quelques exemples concrets :
|
||||||
|
|
||||||
|
| Usage | QCI | Traitement |
|
||||||
|
|---|---|---|
|
||||||
|
| Appel VoLTE (voix sur LTE) | 1 | Latence minimale, débit garanti |
|
||||||
|
| Signalisation réseau | 5 | Très haute priorité |
|
||||||
|
| Visioconférence | 2 | Débit garanti |
|
||||||
|
| Streaming vidéo | 6 ou 8 | Best effort prioritaire |
|
||||||
|
| Web et internet général | 9 | Best effort standard |
|
||||||
|
|
||||||
|
Quand la cellule est encombrée, le routeur sait quoi sacrifier en premier. YouTube va ralentir, les pages web vont mettre du temps à charger, mais l'appel téléphonique de votre voisin reste audible. C'est un compromis assumé : on dégrade volontairement les usages secondaires pour préserver les usages critiques.
|
||||||
|
|
||||||
|
## En 5G : ajouter le découpage
|
||||||
|
|
||||||
|
### Un mécanisme d'accès refondu
|
||||||
|
|
||||||
|
La 5G garde l'esprit du barring mais change son nom et sa mécanique. L'ancien Access Class Barring est remplacé par l'**UAC** — *Unified Access Control*, introduit dans la Release 15 du 3GPP. L'idée est d'unifier dans un seul cadre ce qui était auparavant éparpillé entre ACB, EAB et d'autres mécanismes spécifiques.
|
||||||
|
|
||||||
|
UAC repose sur deux notions. Les **Access Identities** identifient qui vous êtes (utilisateur lambda, abonné à un service prioritaire type MPS ou MCS, personnel d'urgence, agent opérateur). Les **Access Categories** identifient ce que vous essayez de faire (appel d'urgence, connexion data normale, SMS, mise à jour de localisation). La combinaison des deux détermine si votre demande passe ou pas.
|
||||||
|
|
||||||
|
Ce qui change vraiment, c'est la granularité. En 4G, on bloquait une classe entière. En 5G, on peut bloquer un type d'action précis pour un type d'utilisateur précis — par exemple « les abonnés grand public ne peuvent plus initier de nouveaux appels data, mais les SMS et les appels voix continuent ». L'opérateur peut aussi définir ses propres catégories d'accès, calées sur sa politique commerciale et technique.
|
||||||
|
|
||||||
|
### Le QCI devient le 5QI
|
||||||
|
|
||||||
|
Même logique qu'en 4G mais avec plus de finesse. Le **5QI** (*5G QoS Identifier*) propose davantage de niveaux et tient compte de cas que la 4G gérait mal, notamment les services à très basse latence pour les usines connectées ou la voiture autonome. La voix d'urgence garde son sommet, les données critiques industrielles s'intercalent juste après, le streaming et le web restent en bas de la pile.
|
||||||
|
|
||||||
|
### La vraie nouveauté : le network slicing
|
||||||
|
|
||||||
|
C'est l'apport majeur de la 5G en termes de gestion de crise. Au lieu de partager une seule infrastructure entre tous les usages, on peut maintenant la **découper logiciellement en tranches** — des *slices* — qui se comportent comme autant de réseaux indépendants, alors qu'ils tournent sur les mêmes antennes et les mêmes câbles.
|
||||||
|
|
||||||
|
Un opérateur peut par exemple maintenir :
|
||||||
|
|
||||||
|
- une tranche pour le grand public, avec ses millions d'abonnés et son trafic massif,
|
||||||
|
- une tranche pour les services d'urgence et de sécurité, dimensionnée pour rester fluide même quand le reste sature,
|
||||||
|
- une tranche pour les objets connectés industriels, avec des garanties de latence,
|
||||||
|
- une tranche pour les opérateurs critiques type SNCF, EDF, hôpitaux.
|
||||||
|
|
||||||
|
Chaque tranche a ses propres règles d'admission, ses propres priorités, ses propres garanties de performance. Si la tranche grand public est totalement saturée, celle des secours ne le sait même pas. Cette isolation est ce qui distingue le plus fondamentalement la 5G des générations précédentes, où tout le monde se battait pour les mêmes ressources, avec juste des priorités différentes.
|
||||||
|
|
||||||
|
## Et le forfait premium dans tout ça ?
|
||||||
|
|
||||||
|
Question qu'on entend souvent : si je paie un forfait à 50 € au lieu d'un forfait à 10 €, est-ce que je passe avant les autres en cas de saturation ?
|
||||||
|
|
||||||
|
Non.
|
||||||
|
|
||||||
|
Les priorités techniques décrites au-dessus ne dépendent ni du prix du forfait, ni des options commerciales souscrites. Elles dépendent du profil réseau associé à votre SIM (lui-même fonction de votre statut : grand public, secours, opérateur, services prioritaires officiels), et des politiques de gestion de crise programmées par l'opérateur. Un cadre dirigeant avec un forfait illimité reste, du point de vue du réseau, un abonné de classe d'accès 0-9 comme tout le monde.
|
||||||
|
|
||||||
|
Le forfait premium vous donne plus de data, parfois un meilleur débit théorique en conditions normales, des options de roaming, du cloud gratuit. Il ne vous donne pas la priorité face à un pompier ou à un préfet.
|
||||||
|
|
||||||
|
## Pour résumer
|
||||||
|
|
||||||
|
| Génération | Ce qui est contrôlé | Comment |
|
||||||
|
|---|---|---|
|
||||||
|
| 2G | L'accès au réseau | Classes d'accès 0-15 |
|
||||||
|
| 4G | L'accès + la priorité du trafic | ACB / EAB + QCI |
|
||||||
|
| 5G | L'accès + la priorité + l'isolation des services | UAC + 5QI + network slicing |
|
||||||
|
|
||||||
|
Ce qui est intéressant, c'est que ces mécanismes restent invisibles tant que tout va bien. Vous ne savez pas qu'ils existent. Vous découvrez leur existence le jour où votre voisin n'arrive plus à charger ses mails alors que les pompiers, eux, continuent de communiquer normalement. Ce jour-là, ce n'est pas de la magie. C'est trente ans d'ingénierie radio qui ont anticipé que ça arriverait.
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
Un attentat, un séisme, un match du Stade de France, une grande panne d'électricité. Dans ces moments-là, des centaines de milliers de gens dégainent leur téléphone en même temps. Le réseau mobile, qui est dimensionné pour un usage moyen et pas pour un pic massif simultané, devrait théoriquement s'effondrer. La plupart du temps, il tient. Pas parfaitement, pas pour tout le monde, mais il tient — et surtout, les appels d'urgence continuent de passer. C'est le résultat d'une série de mécanismes empilés depuis les années 1990, et que la 4G et la 5G ont raffinés. Voici comment ça marche, sans le jargon mais sans non plus mentir sur ce qui se passe vraiment.
|
||||||
|
|
||||||
|
## Trois questions, pas une
|
||||||
|
|
||||||
|
Dans un réseau cellulaire moderne, l'opérateur doit répondre à trois questions distinctes quand la cellule commence à chauffer. Qui a le droit de se connecter ? Une fois connecté, qui passe en premier ? Et quels services doivent absolument continuer à fonctionner, quoi qu'il arrive ?
|
||||||
|
|
||||||
|
La 2G ne savait répondre qu'à la première. Elle filtrait à l'entrée et basta. La 4G a ajouté la deuxième : une fois admis sur le réseau, votre trafic n'est plus traité de la même manière selon son importance. La 5G ajoute la troisième : elle peut littéralement créer des réseaux virtuels parallèles, dont certains sont réservés à des usages critiques et isolés des autres.
|
||||||
|
|
||||||
|
## En 4G : filtrer puis prioriser
|
||||||
|
|
||||||
|
### Filtrer à l'entrée
|
||||||
|
|
||||||
|
Chaque carte SIM porte un numéro de classe d'accès, hérité du GSM, entre 0 et 15. Les classes 0 à 9 couvrent le grand public — autrement dit nous tous. Les classes 11 à 15 sont réservées : services de secours, autorités publiques, personnel opérateur, usages militaires selon les pays.
|
||||||
|
|
||||||
|
Quand une cellule est surchargée, l'eNodeB (la station de base 4G) diffuse une consigne aux téléphones du secteur : « les classes 0 à 9, vous attendez ». C'est l'**Access Class Barring**. Concrètement, votre téléphone reçoit ce message et bloque lui-même votre tentative d'appel ou de connexion data, sans même envoyer la demande à la station. C'est élégant parce que ça soulage la station avant même qu'elle ne soit sollicitée. Les classes prioritaires, elles, passent sans encombre.
|
||||||
|
|
||||||
|
Il existe une variante plus dure appelée **Extended Access Barring**, conçue pour les objets connectés et les usages non urgents. Quand une vraie crise se déclare, l'opérateur peut couper les compteurs intelligents, les alarmes domestiques et autres équipements bavards pour préserver la bande passante humaine.
|
||||||
|
|
||||||
|
### Prioriser une fois connecté
|
||||||
|
|
||||||
|
Là où la 4G a vraiment innové, c'est en introduisant le **QCI** — *QoS Class Identifier*. Chaque flux de données qui transite sur le réseau se voit attribuer un numéro entre 1 et 9 (et quelques valeurs au-dessus pour des cas spéciaux) qui dit à l'infrastructure comment le traiter.
|
||||||
|
|
||||||
|
Quelques exemples concrets :
|
||||||
|
|
||||||
|
| Usage | QCI | Traitement |
|
||||||
|
|---|---|---|
|
||||||
|
| Appel VoLTE (voix sur LTE) | 1 | Latence minimale, débit garanti |
|
||||||
|
| Signalisation réseau | 5 | Très haute priorité |
|
||||||
|
| Visioconférence | 2 | Débit garanti |
|
||||||
|
| Streaming vidéo | 6 ou 8 | Best effort prioritaire |
|
||||||
|
| Web et internet général | 9 | Best effort standard |
|
||||||
|
|
||||||
|
Quand la cellule est encombrée, le routeur sait quoi sacrifier en premier. YouTube va ralentir, les pages web vont mettre du temps à charger, mais l'appel téléphonique de votre voisin reste audible. C'est un compromis assumé : on dégrade volontairement les usages secondaires pour préserver les usages critiques.
|
||||||
|
|
||||||
|
## En 5G : ajouter le découpage
|
||||||
|
|
||||||
|
### Un mécanisme d'accès refondu
|
||||||
|
|
||||||
|
La 5G garde l'esprit du barring mais change son nom et sa mécanique. L'ancien Access Class Barring est remplacé par l'**UAC** — *Unified Access Control*, introduit dans la Release 15 du 3GPP. L'idée est d'unifier dans un seul cadre ce qui était auparavant éparpillé entre ACB, EAB et d'autres mécanismes spécifiques.
|
||||||
|
|
||||||
|
UAC repose sur deux notions. Les **Access Identities** identifient qui vous êtes (utilisateur lambda, abonné à un service prioritaire type MPS ou MCS, personnel d'urgence, agent opérateur). Les **Access Categories** identifient ce que vous essayez de faire (appel d'urgence, connexion data normale, SMS, mise à jour de localisation). La combinaison des deux détermine si votre demande passe ou pas.
|
||||||
|
|
||||||
|
Ce qui change vraiment, c'est la granularité. En 4G, on bloquait une classe entière. En 5G, on peut bloquer un type d'action précis pour un type d'utilisateur précis — par exemple « les abonnés grand public ne peuvent plus initier de nouveaux appels data, mais les SMS et les appels voix continuent ». L'opérateur peut aussi définir ses propres catégories d'accès, calées sur sa politique commerciale et technique.
|
||||||
|
|
||||||
|
### Le QCI devient le 5QI
|
||||||
|
|
||||||
|
Même logique qu'en 4G mais avec plus de finesse. Le **5QI** (*5G QoS Identifier*) propose davantage de niveaux et tient compte de cas que la 4G gérait mal, notamment les services à très basse latence pour les usines connectées ou la voiture autonome. La voix d'urgence garde son sommet, les données critiques industrielles s'intercalent juste après, le streaming et le web restent en bas de la pile.
|
||||||
|
|
||||||
|
### La vraie nouveauté : le network slicing
|
||||||
|
|
||||||
|
C'est l'apport majeur de la 5G en termes de gestion de crise. Au lieu de partager une seule infrastructure entre tous les usages, on peut maintenant la **découper logiciellement en tranches** — des *slices* — qui se comportent comme autant de réseaux indépendants, alors qu'ils tournent sur les mêmes antennes et les mêmes câbles.
|
||||||
|
|
||||||
|
Un opérateur peut par exemple maintenir :
|
||||||
|
|
||||||
|
- une tranche pour le grand public, avec ses millions d'abonnés et son trafic massif,
|
||||||
|
- une tranche pour les services d'urgence et de sécurité, dimensionnée pour rester fluide même quand le reste sature,
|
||||||
|
- une tranche pour les objets connectés industriels, avec des garanties de latence,
|
||||||
|
- une tranche pour les opérateurs critiques type SNCF, EDF, hôpitaux.
|
||||||
|
|
||||||
|
Chaque tranche a ses propres règles d'admission, ses propres priorités, ses propres garanties de performance. Si la tranche grand public est totalement saturée, celle des secours ne le sait même pas. Cette isolation est ce qui distingue le plus fondamentalement la 5G des générations précédentes, où tout le monde se battait pour les mêmes ressources, avec juste des priorités différentes.
|
||||||
|
|
||||||
|
## Et le forfait premium dans tout ça ?
|
||||||
|
|
||||||
|
Question qu'on entend souvent : si je paie un forfait à 50 € au lieu d'un forfait à 10 €, est-ce que je passe avant les autres en cas de saturation ?
|
||||||
|
|
||||||
|
Non.
|
||||||
|
|
||||||
|
Les priorités techniques décrites au-dessus ne dépendent ni du prix du forfait, ni des options commerciales souscrites. Elles dépendent du profil réseau associé à votre SIM (lui-même fonction de votre statut : grand public, secours, opérateur, services prioritaires officiels), et des politiques de gestion de crise programmées par l'opérateur. Un cadre dirigeant avec un forfait illimité reste, du point de vue du réseau, un abonné de classe d'accès 0-9 comme tout le monde.
|
||||||
|
|
||||||
|
Le forfait premium vous donne plus de data, parfois un meilleur débit théorique en conditions normales, des options de roaming, du cloud gratuit. Il ne vous donne pas la priorité face à un pompier ou à un préfet.
|
||||||
|
|
||||||
|
## Pour résumer
|
||||||
|
|
||||||
|
| Génération | Ce qui est contrôlé | Comment |
|
||||||
|
|---|---|---|
|
||||||
|
| 2G | L'accès au réseau | Classes d'accès 0-15 |
|
||||||
|
| 4G | L'accès + la priorité du trafic | ACB / EAB + QCI |
|
||||||
|
| 5G | L'accès + la priorité + l'isolation des services | UAC + 5QI + network slicing |
|
||||||
|
|
||||||
|
Ce qui est intéressant, c'est que ces mécanismes restent invisibles tant que tout va bien. Vous ne savez pas qu'ils existent. Vous découvrez leur existence le jour où votre voisin n'arrive plus à charger ses mails alors que les pompiers, eux, continuent de communiquer normalement. Ce jour-là, ce n'est pas de la magie. C'est trente ans d'ingénierie radio qui ont anticipé que ça arriverait.
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
Un attentat, un séisme, un match du Stade de France, une grande panne d'électricité. Dans ces moments-là, des centaines de milliers de gens dégainent leur téléphone en même temps. Le réseau mobile, qui est dimensionné pour un usage moyen et pas pour un pic massif simultané, devrait théoriquement s'effondrer. La plupart du temps, il tient. Pas parfaitement, pas pour tout le monde, mais il tient — et surtout, les appels d'urgence continuent de passer. C'est le résultat d'une série de mécanismes empilés depuis les années 1990, et que la 4G et la 5G ont raffinés. Voici comment ça marche, sans le jargon mais sans non plus mentir sur ce qui se passe vraiment.
|
||||||
|
|
||||||
|
## Trois questions, pas une
|
||||||
|
|
||||||
|
Dans un réseau cellulaire moderne, l'opérateur doit répondre à trois questions distinctes quand la cellule commence à chauffer. Qui a le droit de se connecter ? Une fois connecté, qui passe en premier ? Et quels services doivent absolument continuer à fonctionner, quoi qu'il arrive ?
|
||||||
|
|
||||||
|
La 2G ne savait répondre qu'à la première. Elle filtrait à l'entrée et basta. La 4G a ajouté la deuxième : une fois admis sur le réseau, votre trafic n'est plus traité de la même manière selon son importance. La 5G ajoute la troisième : elle peut littéralement créer des réseaux virtuels parallèles, dont certains sont réservés à des usages critiques et isolés des autres.
|
||||||
|
|
||||||
|
## En 4G : filtrer puis prioriser
|
||||||
|
|
||||||
|
### Filtrer à l'entrée
|
||||||
|
|
||||||
|
Chaque carte SIM porte un numéro de classe d'accès, hérité du GSM, entre 0 et 15. Les classes 0 à 9 couvrent le grand public — autrement dit nous tous. Les classes 11 à 15 sont réservées : services de secours, autorités publiques, personnel opérateur, usages militaires selon les pays.
|
||||||
|
|
||||||
|
Quand une cellule est surchargée, l'eNodeB (la station de base 4G) diffuse une consigne aux téléphones du secteur : « les classes 0 à 9, vous attendez ». C'est l'**Access Class Barring**. Concrètement, votre téléphone reçoit ce message et bloque lui-même votre tentative d'appel ou de connexion data, sans même envoyer la demande à la station. C'est élégant parce que ça soulage la station avant même qu'elle ne soit sollicitée. Les classes prioritaires, elles, passent sans encombre.
|
||||||
|
|
||||||
|
Il existe une variante plus dure appelée **Extended Access Barring**, conçue pour les objets connectés et les usages non urgents. Quand une vraie crise se déclare, l'opérateur peut couper les compteurs intelligents, les alarmes domestiques et autres équipements bavards pour préserver la bande passante humaine.
|
||||||
|
|
||||||
|
### Prioriser une fois connecté
|
||||||
|
|
||||||
|
Là où la 4G a vraiment innové, c'est en introduisant le **QCI** — *QoS Class Identifier*. Chaque flux de données qui transite sur le réseau se voit attribuer un numéro entre 1 et 9 (et quelques valeurs au-dessus pour des cas spéciaux) qui dit à l'infrastructure comment le traiter.
|
||||||
|
|
||||||
|
Quelques exemples concrets :
|
||||||
|
|
||||||
|
| Usage | QCI | Traitement |
|
||||||
|
|---|---|---|
|
||||||
|
| Appel VoLTE (voix sur LTE) | 1 | Latence minimale, débit garanti |
|
||||||
|
| Signalisation réseau | 5 | Très haute priorité |
|
||||||
|
| Visioconférence | 2 | Débit garanti |
|
||||||
|
| Streaming vidéo | 6 ou 8 | Best effort prioritaire |
|
||||||
|
| Web et internet général | 9 | Best effort standard |
|
||||||
|
|
||||||
|
Quand la cellule est encombrée, le routeur sait quoi sacrifier en premier. YouTube va ralentir, les pages web vont mettre du temps à charger, mais l'appel téléphonique de votre voisin reste audible. C'est un compromis assumé : on dégrade volontairement les usages secondaires pour préserver les usages critiques.
|
||||||
|
|
||||||
|
## En 5G : ajouter le découpage
|
||||||
|
|
||||||
|
### Un mécanisme d'accès refondu
|
||||||
|
|
||||||
|
La 5G garde l'esprit du barring mais change son nom et sa mécanique. L'ancien Access Class Barring est remplacé par l'**UAC** — *Unified Access Control*, introduit dans la Release 15 du 3GPP. L'idée est d'unifier dans un seul cadre ce qui était auparavant éparpillé entre ACB, EAB et d'autres mécanismes spécifiques.
|
||||||
|
|
||||||
|
UAC repose sur deux notions. Les **Access Identities** identifient qui vous êtes (utilisateur lambda, abonné à un service prioritaire type MPS ou MCS, personnel d'urgence, agent opérateur). Les **Access Categories** identifient ce que vous essayez de faire (appel d'urgence, connexion data normale, SMS, mise à jour de localisation). La combinaison des deux détermine si votre demande passe ou pas.
|
||||||
|
|
||||||
|
Ce qui change vraiment, c'est la granularité. En 4G, on bloquait une classe entière. En 5G, on peut bloquer un type d'action précis pour un type d'utilisateur précis — par exemple « les abonnés grand public ne peuvent plus initier de nouveaux appels data, mais les SMS et les appels voix continuent ». L'opérateur peut aussi définir ses propres catégories d'accès, calées sur sa politique commerciale et technique.
|
||||||
|
|
||||||
|
### Le QCI devient le 5QI
|
||||||
|
|
||||||
|
Même logique qu'en 4G mais avec plus de finesse. Le **5QI** (*5G QoS Identifier*) propose davantage de niveaux et tient compte de cas que la 4G gérait mal, notamment les services à très basse latence pour les usines connectées ou la voiture autonome. La voix d'urgence garde son sommet, les données critiques industrielles s'intercalent juste après, le streaming et le web restent en bas de la pile.
|
||||||
|
|
||||||
|
### La vraie nouveauté : le network slicing
|
||||||
|
|
||||||
|
C'est l'apport majeur de la 5G en termes de gestion de crise. Au lieu de partager une seule infrastructure entre tous les usages, on peut maintenant la **découper logiciellement en tranches** — des *slices* — qui se comportent comme autant de réseaux indépendants, alors qu'ils tournent sur les mêmes antennes et les mêmes câbles.
|
||||||
|
|
||||||
|
Un opérateur peut par exemple maintenir :
|
||||||
|
|
||||||
|
- une tranche pour le grand public, avec ses millions d'abonnés et son trafic massif,
|
||||||
|
- une tranche pour les services d'urgence et de sécurité, dimensionnée pour rester fluide même quand le reste sature,
|
||||||
|
- une tranche pour les objets connectés industriels, avec des garanties de latence,
|
||||||
|
- une tranche pour les opérateurs critiques type SNCF, EDF, hôpitaux.
|
||||||
|
|
||||||
|
Chaque tranche a ses propres règles d'admission, ses propres priorités, ses propres garanties de performance. Si la tranche grand public est totalement saturée, celle des secours ne le sait même pas. Cette isolation est ce qui distingue le plus fondamentalement la 5G des générations précédentes, où tout le monde se battait pour les mêmes ressources, avec juste des priorités différentes.
|
||||||
|
|
||||||
|
## Et le forfait premium dans tout ça ?
|
||||||
|
|
||||||
|
Question qu'on entend souvent : si je paie un forfait à 50 € au lieu d'un forfait à 10 €, est-ce que je passe avant les autres en cas de saturation ?
|
||||||
|
|
||||||
|
Non.
|
||||||
|
|
||||||
|
Les priorités techniques décrites au-dessus ne dépendent ni du prix du forfait, ni des options commerciales souscrites. Elles dépendent du profil réseau associé à votre SIM (lui-même fonction de votre statut : grand public, secours, opérateur, services prioritaires officiels), et des politiques de gestion de crise programmées par l'opérateur. Un cadre dirigeant avec un forfait illimité reste, du point de vue du réseau, un abonné de classe d'accès 0-9 comme tout le monde.
|
||||||
|
|
||||||
|
Le forfait premium vous donne plus de data, parfois un meilleur débit théorique en conditions normales, des options de roaming, du cloud gratuit. Il ne vous donne pas la priorité face à un pompier ou à un préfet.
|
||||||
|
|
||||||
|
## Pour résumer
|
||||||
|
|
||||||
|
| Génération | Ce qui est contrôlé | Comment |
|
||||||
|
|---|---|---|
|
||||||
|
| 2G | L'accès au réseau | Classes d'accès 0-15 |
|
||||||
|
| 4G | L'accès + la priorité du trafic | ACB / EAB + QCI |
|
||||||
|
| 5G | L'accès + la priorité + l'isolation des services | UAC + 5QI + network slicing |
|
||||||
|
|
||||||
|
Ce qui est intéressant, c'est que ces mécanismes restent invisibles tant que tout va bien. Vous ne savez pas qu'ils existent. Vous découvrez leur existence le jour où votre voisin n'arrive plus à charger ses mails alors que les pompiers, eux, continuent de communiquer normalement. Ce jour-là, ce n'est pas de la magie. C'est trente ans d'ingénierie radio qui ont anticipé que ça arriverait.
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
Un attentat, un séisme, un match du Stade de France, une grande panne d'électricité. Dans ces moments-là, des centaines de milliers de gens dégainent leur téléphone en même temps. Le réseau mobile, qui est dimensionné pour un usage moyen et pas pour un pic massif simultané, devrait théoriquement s'effondrer. La plupart du temps, il tient. Pas parfaitement, pas pour tout le monde, mais il tient — et surtout, les appels d'urgence continuent de passer. C'est le résultat d'une série de mécanismes empilés depuis les années 1990, et que la 4G et la 5G ont raffinés. Voici comment ça marche, sans le jargon mais sans non plus mentir sur ce qui se passe vraiment.
|
||||||
|
|
||||||
|
## Trois questions, pas une
|
||||||
|
|
||||||
|
Dans un réseau cellulaire moderne, l'opérateur doit répondre à trois questions distinctes quand la cellule commence à chauffer. Qui a le droit de se connecter ? Une fois connecté, qui passe en premier ? Et quels services doivent absolument continuer à fonctionner, quoi qu'il arrive ?
|
||||||
|
|
||||||
|
La 2G ne savait répondre qu'à la première. Elle filtrait à l'entrée et basta. La 4G a ajouté la deuxième : une fois admis sur le réseau, votre trafic n'est plus traité de la même manière selon son importance. La 5G ajoute la troisième : elle peut littéralement créer des réseaux virtuels parallèles, dont certains sont réservés à des usages critiques et isolés des autres.
|
||||||
|
|
||||||
|
## En 4G : filtrer puis prioriser
|
||||||
|
|
||||||
|
### Filtrer à l'entrée
|
||||||
|
|
||||||
|
Chaque carte SIM porte un numéro de classe d'accès, hérité du GSM, entre 0 et 15. Les classes 0 à 9 couvrent le grand public — autrement dit nous tous. Les classes 11 à 15 sont réservées : services de secours, autorités publiques, personnel opérateur, usages militaires selon les pays.
|
||||||
|
|
||||||
|
Quand une cellule est surchargée, l'eNodeB (la station de base 4G) diffuse une consigne aux téléphones du secteur : « les classes 0 à 9, vous attendez ». C'est l'**Access Class Barring**. Concrètement, votre téléphone reçoit ce message et bloque lui-même votre tentative d'appel ou de connexion data, sans même envoyer la demande à la station. C'est élégant parce que ça soulage la station avant même qu'elle ne soit sollicitée. Les classes prioritaires, elles, passent sans encombre.
|
||||||
|
|
||||||
|
Il existe une variante plus dure appelée **Extended Access Barring**, conçue pour les objets connectés et les usages non urgents. Quand une vraie crise se déclare, l'opérateur peut couper les compteurs intelligents, les alarmes domestiques et autres équipements bavards pour préserver la bande passante humaine.
|
||||||
|
|
||||||
|
### Prioriser une fois connecté
|
||||||
|
|
||||||
|
Là où la 4G a vraiment innové, c'est en introduisant le **QCI** — *QoS Class Identifier*. Chaque flux de données qui transite sur le réseau se voit attribuer un numéro entre 1 et 9 (et quelques valeurs au-dessus pour des cas spéciaux) qui dit à l'infrastructure comment le traiter.
|
||||||
|
|
||||||
|
Quelques exemples concrets :
|
||||||
|
|
||||||
|
| Usage | QCI | Traitement |
|
||||||
|
|---|---|---|
|
||||||
|
| Appel VoLTE (voix sur LTE) | 1 | Latence minimale, débit garanti |
|
||||||
|
| Signalisation réseau | 5 | Très haute priorité |
|
||||||
|
| Visioconférence | 2 | Débit garanti |
|
||||||
|
| Streaming vidéo | 6 ou 8 | Best effort prioritaire |
|
||||||
|
| Web et internet général | 9 | Best effort standard |
|
||||||
|
|
||||||
|
Quand la cellule est encombrée, le routeur sait quoi sacrifier en premier. YouTube va ralentir, les pages web vont mettre du temps à charger, mais l'appel téléphonique de votre voisin reste audible. C'est un compromis assumé : on dégrade volontairement les usages secondaires pour préserver les usages critiques.
|
||||||
|
|
||||||
|
## En 5G : ajouter le découpage
|
||||||
|
|
||||||
|
### Un mécanisme d'accès refondu
|
||||||
|
|
||||||
|
La 5G garde l'esprit du barring mais change son nom et sa mécanique. L'ancien Access Class Barring est remplacé par l'**UAC** — *Unified Access Control*, introduit dans la Release 15 du 3GPP. L'idée est d'unifier dans un seul cadre ce qui était auparavant éparpillé entre ACB, EAB et d'autres mécanismes spécifiques.
|
||||||
|
|
||||||
|
UAC repose sur deux notions. Les **Access Identities** identifient qui vous êtes (utilisateur lambda, abonné à un service prioritaire type MPS ou MCS, personnel d'urgence, agent opérateur). Les **Access Categories** identifient ce que vous essayez de faire (appel d'urgence, connexion data normale, SMS, mise à jour de localisation). La combinaison des deux détermine si votre demande passe ou pas.
|
||||||
|
|
||||||
|
Ce qui change vraiment, c'est la granularité. En 4G, on bloquait une classe entière. En 5G, on peut bloquer un type d'action précis pour un type d'utilisateur précis — par exemple « les abonnés grand public ne peuvent plus initier de nouveaux appels data, mais les SMS et les appels voix continuent ». L'opérateur peut aussi définir ses propres catégories d'accès, calées sur sa politique commerciale et technique.
|
||||||
|
|
||||||
|
### Le QCI devient le 5QI
|
||||||
|
|
||||||
|
Même logique qu'en 4G mais avec plus de finesse. Le **5QI** (*5G QoS Identifier*) propose davantage de niveaux et tient compte de cas que la 4G gérait mal, notamment les services à très basse latence pour les usines connectées ou la voiture autonome. La voix d'urgence garde son sommet, les données critiques industrielles s'intercalent juste après, le streaming et le web restent en bas de la pile.
|
||||||
|
|
||||||
|
### La vraie nouveauté : le network slicing
|
||||||
|
|
||||||
|
C'est l'apport majeur de la 5G en termes de gestion de crise. Au lieu de partager une seule infrastructure entre tous les usages, on peut maintenant la **découper logiciellement en tranches** — des *slices* — qui se comportent comme autant de réseaux indépendants, alors qu'ils tournent sur les mêmes antennes et les mêmes câbles.
|
||||||
|
|
||||||
|
Un opérateur peut par exemple maintenir :
|
||||||
|
|
||||||
|
- une tranche pour le grand public, avec ses millions d'abonnés et son trafic massif,
|
||||||
|
- une tranche pour les services d'urgence et de sécurité, dimensionnée pour rester fluide même quand le reste sature,
|
||||||
|
- une tranche pour les objets connectés industriels, avec des garanties de latence,
|
||||||
|
- une tranche pour les opérateurs critiques type SNCF, EDF, hôpitaux.
|
||||||
|
|
||||||
|
Chaque tranche a ses propres règles d'admission, ses propres priorités, ses propres garanties de performance. Si la tranche grand public est totalement saturée, celle des secours ne le sait même pas. Cette isolation est ce qui distingue le plus fondamentalement la 5G des générations précédentes, où tout le monde se battait pour les mêmes ressources, avec juste des priorités différentes.
|
||||||
|
|
||||||
|
## Et le forfait premium dans tout ça ?
|
||||||
|
|
||||||
|
Question qu'on entend souvent : si je paie un forfait à 50 € au lieu d'un forfait à 10 €, est-ce que je passe avant les autres en cas de saturation ?
|
||||||
|
|
||||||
|
Non.
|
||||||
|
|
||||||
|
Les priorités techniques décrites au-dessus ne dépendent ni du prix du forfait, ni des options commerciales souscrites. Elles dépendent du profil réseau associé à votre SIM (lui-même fonction de votre statut : grand public, secours, opérateur, services prioritaires officiels), et des politiques de gestion de crise programmées par l'opérateur. Un cadre dirigeant avec un forfait illimité reste, du point de vue du réseau, un abonné de classe d'accès 0-9 comme tout le monde.
|
||||||
|
|
||||||
|
Le forfait premium vous donne plus de data, parfois un meilleur débit théorique en conditions normales, des options de roaming, du cloud gratuit. Il ne vous donne pas la priorité face à un pompier ou à un préfet.
|
||||||
|
|
||||||
|
## Pour résumer
|
||||||
|
|
||||||
|
| Génération | Ce qui est contrôlé | Comment |
|
||||||
|
|---|---|---|
|
||||||
|
| 2G | L'accès au réseau | Classes d'accès 0-15 |
|
||||||
|
| 4G | L'accès + la priorité du trafic | ACB / EAB + QCI |
|
||||||
|
| 5G | L'accès + la priorité + l'isolation des services | UAC + 5QI + network slicing |
|
||||||
|
|
||||||
|
Ce qui est intéressant, c'est que ces mécanismes restent invisibles tant que tout va bien. Vous ne savez pas qu'ils existent. Vous découvrez leur existence le jour où votre voisin n'arrive plus à charger ses mails alors que les pompiers, eux, continuent de communiquer normalement. Ce jour-là, ce n'est pas de la magie. C'est trente ans d'ingénierie radio qui ont anticipé que ça arriverait.
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
Un attentat, un séisme, un match du Stade de France, une grande panne d'électricité. Dans ces moments-là, des centaines de milliers de gens dégainent leur téléphone au même instant. Le réseau mobile est dimensionné pour un usage moyen, pas pour un pic massif simultané, et il devrait théoriquement s'effondrer. La plupart du temps, il tient. Pas parfaitement, pas pour tout le monde, mais il tient — et surtout, les appels d'urgence continuent de passer. C'est le résultat d'une série de mécanismes empilés depuis les années 1990, que la 4G a affinés et que la 5G a élargis. Cet article les passe en revue, et termine sur une question qu'on me pose souvent : est-ce que mon forfait à 50 € me donne une place prioritaire dans cette file d'attente ?
|
||||||
|
|
||||||
|
## Trois questions, pas une
|
||||||
|
|
||||||
|
Quand une cellule commence à chauffer, l'opérateur doit répondre à trois questions distinctes. Qui a le droit de se connecter ? Une fois connecté, qui passe en premier ? Et quels services doivent absolument continuer à fonctionner, quoi qu'il arrive ?
|
||||||
|
|
||||||
|
La 2G ne savait répondre qu'à la première. Elle filtrait à l'entrée et basta. La 4G a ajouté la deuxième : une fois admis sur le réseau, votre trafic est traité différemment selon son importance. La 5G ajoute la troisième : elle peut créer des réseaux virtuels parallèles dont certains sont réservés à des usages critiques, totalement isolés des autres.
|
||||||
|
|
||||||
|
## Le filtrage à l'entrée
|
||||||
|
|
||||||
|
Chaque carte SIM porte un numéro de classe d'accès, hérité du GSM, entre 0 et 15. Les classes 0 à 9 couvrent le grand public — autrement dit nous tous. Les classes 11 à 15 sont réservées : services de secours, autorités publiques, personnel opérateur, usages militaires selon les pays.
|
||||||
|
|
||||||
|
Quand une cellule est surchargée, l'eNodeB (la station de base 4G) diffuse une consigne aux téléphones du secteur : « les classes 0 à 9, vous attendez ». C'est l'**Access Class Barring**. Concrètement, votre téléphone reçoit ce message et bloque lui-même votre tentative d'appel ou de connexion data, sans même envoyer la demande à la station. C'est élégant parce que ça soulage la station avant même qu'elle ne soit sollicitée. Les classes prioritaires, elles, passent sans encombre.
|
||||||
|
|
||||||
|
Une variante plus dure, l'**Extended Access Barring**, vise les objets connectés et les usages non urgents. Quand une vraie crise se déclare, l'opérateur peut couper les compteurs intelligents, les alarmes domestiques et autres équipements bavards pour préserver la bande passante humaine.
|
||||||
|
|
||||||
|
En 5G, ce mécanisme a été refondu sous le nom d'**UAC** — *Unified Access Control*, introduit dans la Release 15 du 3GPP. UAC unifie dans un seul cadre ce qui était auparavant éparpillé entre ACB, EAB et d'autres dispositifs spécifiques. Il repose sur deux notions complémentaires. Les *Access Identities* identifient qui vous êtes : utilisateur lambda, abonné à un service prioritaire type MPS ou MCS, personnel d'urgence, agent opérateur. Les *Access Categories* identifient ce que vous essayez de faire : appel d'urgence, connexion data normale, SMS, mise à jour de localisation. La combinaison des deux détermine si votre demande passe ou pas. La granularité gagnée par rapport à la 4G est réelle : on peut bloquer un type d'action précis pour un type d'utilisateur précis, par exemple « les abonnés grand public ne peuvent plus initier de nouveaux appels data, mais les SMS et les appels voix continuent ».
|
||||||
|
|
||||||
|
## La priorité une fois connecté
|
||||||
|
|
||||||
|
Là où la 4G a vraiment innové, c'est en introduisant le **QCI** — *QoS Class Identifier*. Chaque flux de données qui transite sur le réseau se voit attribuer un numéro entre 1 et 9 (avec quelques valeurs supplémentaires pour des cas spéciaux) qui dit à l'infrastructure comment le traiter.
|
||||||
|
|
||||||
|
| Usage | QCI | Traitement |
|
||||||
|
|---|---|---|
|
||||||
|
| Appel VoLTE (voix sur LTE) | 1 | Latence minimale, débit garanti |
|
||||||
|
| Visioconférence | 2 | Débit garanti |
|
||||||
|
| Signalisation réseau | 5 | Très haute priorité |
|
||||||
|
| Streaming vidéo | 6 ou 8 | Best effort prioritaire |
|
||||||
|
| Web et internet général | 9 | Best effort standard |
|
||||||
|
|
||||||
|
Quand la cellule est encombrée, le routeur sait quoi sacrifier en premier. YouTube va ralentir, les pages web vont mettre du temps à charger, mais l'appel téléphonique de votre voisin reste audible. C'est un compromis assumé : on dégrade volontairement les usages secondaires pour préserver les usages critiques.
|
||||||
|
|
||||||
|
La 5G a transposé ce mécanisme sous le nom de **5QI** (*5G QoS Identifier*) avec davantage de niveaux et une meilleure prise en compte des cas que la 4G gérait mal — notamment les services à très basse latence pour les usines connectées ou la voiture autonome. La voix d'urgence garde son sommet, les données critiques industrielles s'intercalent juste après, le streaming et le web restent en bas de la pile.
|
||||||
|
|
||||||
|
## L'isolation par tranches : le network slicing
|
||||||
|
|
||||||
|
C'est l'apport majeur de la 5G en matière de gestion de crise. Au lieu de partager une seule infrastructure entre tous les usages, on peut maintenant la découper logiciellement en tranches — des *slices* — qui se comportent comme autant de réseaux indépendants, alors qu'ils tournent sur les mêmes antennes et les mêmes câbles.
|
||||||
|
|
||||||
|
Un opérateur peut par exemple maintenir une tranche pour le grand public avec ses millions d'abonnés et son trafic massif, une autre pour les services d'urgence dimensionnée pour rester fluide même quand le reste sature, une troisième pour les objets connectés industriels avec des garanties de latence, et une quatrième pour des opérateurs critiques type SNCF, EDF ou hôpitaux. Chaque tranche a ses propres règles d'admission, ses propres priorités, ses propres garanties de performance. Si la tranche grand public est totalement saturée, celle des secours ne le sait même pas.
|
||||||
|
|
||||||
|
Cette isolation est ce qui distingue le plus fondamentalement la 5G des générations précédentes. Avant, tout le monde se battait pour les mêmes ressources, avec juste des priorités différentes pour départager. Maintenant, certaines ressources sont retirées du combat dès le départ.
|
||||||
|
|
||||||
|
## Récapitulatif
|
||||||
|
|
||||||
|
| Génération | Ce qui est contrôlé | Comment |
|
||||||
|
|---|---|---|
|
||||||
|
| 2G | L'accès au réseau | Classes d'accès 0-15 |
|
||||||
|
| 4G | L'accès + la priorité du trafic | ACB / EAB + QCI |
|
||||||
|
| 5G | L'accès + la priorité + l'isolation des services | UAC + 5QI + network slicing |
|
||||||
|
|
||||||
|
Tous ces mécanismes restent invisibles tant que tout va bien. Vous ne savez pas qu'ils existent. Vous découvrez leur existence le jour où votre voisin n'arrive plus à charger ses mails alors que les pompiers, eux, continuent de communiquer normalement. Ce jour-là, ce n'est pas de la magie. C'est trente ans d'ingénierie radio qui ont anticipé que ça arriverait.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Et mon forfait premium, alors ?
|
||||||
|
|
||||||
|
Question logique à ce stade. Si le réseau sait techniquement prioriser certains flux par rapport à d'autres, qu'est-ce qui empêche un opérateur de faire passer ses abonnés à 50 € devant ceux à 10 € quand les antennes saturent ? La réponse honnête commence par un aveu : techniquement, rien. L'outil existe, il s'appelle **Quality of Service** (QoS), c'est exactement le mécanisme qu'on vient de décrire. Si demain Orange ou SFR voulaient créer une voie rapide pour leurs abonnés haut de gamme, ils auraient les outils dans la boîte. Pourtant, ils ne le font pas. Pour quatre raisons.
|
||||||
|
|
||||||
|
### La loi européenne l'interdit
|
||||||
|
|
||||||
|
Le règlement **(UE) 2015/2120**, dit « règlement internet ouvert », oblige les opérateurs à traiter tout le trafic de la même façon, sans discrimination liée à l'expéditeur, au destinataire, au contenu ou à l'application. Il a fêté ses dix ans en novembre 2025, et l'ARCEP a profité de l'anniversaire pour rappeler que c'est l'un des piliers du modèle numérique européen. Les sanctions sont sérieuses : jusqu'à **3 % du chiffre d'affaires** de l'opérateur fautif. Un opérateur français qui annoncerait demain « avec notre forfait Premium, vous passez devant les autres » se retrouverait devant l'ARCEP dans la semaine.
|
||||||
|
|
||||||
|
Le règlement laisse quelques portes ouvertes pour les services dits « spécialisés » qui ont besoin d'une qualité garantie — téléchirurgie, voiture connectée. Mais ces exceptions sont étroitement encadrées et ne couvrent absolument pas le confort d'un client haut de gamme qui voudrait charger son Instagram plus vite à 19h.
|
||||||
|
|
||||||
|
Aux États-Unis, l'histoire est différente. La FCC a tenté de restaurer la neutralité du net en 2024, mais en janvier 2025 la cour d'appel du sixième circuit a invalidé la décision, jugeant que la FCC n'avait pas l'autorité légale pour reclasser le haut débit comme service public. Avec l'arrivée de Brendan Carr à la tête de la FCC, ouvertement opposé à la neutralité du net, il n'y a aujourd'hui plus de règle fédérale outre-Atlantique. Quelques États (Californie, Washington, New York, Oregon) ont leurs propres lois qui maintiennent le principe, mais à l'échelle du pays, les opérateurs américains pourraient légalement faire ce que leurs homologues européens n'ont pas le droit de faire. Pourtant, ils ne le font pas ouvertement non plus, et la raison renvoie aux trois points suivants.
|
||||||
|
|
||||||
|
### C'est commercialement intenable
|
||||||
|
|
||||||
|
Imagine la publicité : « Forfait Premium à 50 € — passez devant les pauvres pendant les heures de pointe ». Le slogan ne se vend pas. Les directions marketing savent que dire à la moitié de leurs clients qu'ils sont des citoyens de seconde zone du réseau est le plus court chemin vers une crise de réputation. C'est pour ça qu'on vous vend « plus de Go », « 5G ultra rapide », « roaming inclus dans 110 pays » — des promesses qui sonnent positivement sans jamais dire à personne qu'il est désavantagé.
|
||||||
|
|
||||||
|
### L'effet boule de neige serait toxique
|
||||||
|
|
||||||
|
Imagine que ça se mette quand même en place. Les riches passent devant. Les antennes restent saturées pour les autres, qui se mettent à payer plus pour échapper à la saturation, ce qui sature encore plus les bas forfaits, ce qui pousse encore plus de gens à monter en gamme. Au bout de cinq ans, on a un réseau à deux vitesses où les forfaits modestes deviennent quasi inutilisables aux heures critiques, et où la connexion mobile correcte devient un service de luxe. Ce n'est plus un service de télécommunications, c'est un système de classes.
|
||||||
|
|
||||||
|
C'est exactement ce que la neutralité du net cherche à empêcher. Pas par idéologie, mais parce qu'on a déjà vu où mène ce genre de spirale dans les pays où elle n'est pas protégée. Certains opérateurs proposent par exemple des forfaits où Facebook et WhatsApp sont gratuits mais où le reste est payant, ce qui revient à dire que le bon internet est celui que l'opérateur a choisi pour vous. Ce n'est plus tout à fait le même service.
|
||||||
|
|
||||||
|
### Ça ne résoudrait rien
|
||||||
|
|
||||||
|
Quand un réseau sature, ce n'est pas un problème de répartition entre utilisateurs, c'est un problème de **capacité totale**. Faire passer Pierre avant Paul ne crée pas un seul bit de bande passante supplémentaire. Ça déplace juste le problème de l'un vers l'autre. La vraie solution, quand une cellule sature trop souvent, c'est d'installer plus d'antennes, de densifier le réseau, de basculer sur une fréquence plus performante ou de passer à la génération suivante. C'est cher, c'est long, ça implique des autorisations administratives et des négociations foncières, mais c'est la seule réponse qui tient la route. Prioriser, c'est rapide, mais ça repousse le mur, ça ne le déplace pas.
|
||||||
|
|
||||||
|
C'est comme si on proposait une voie réservée aux Mercedes sur l'A7 un samedi de chassé-croisé. Techniquement, on peut peindre la ligne au sol et installer les panneaux dans la matinée. Mais cette voie ne réduit pas le bouchon, elle le concentre sur les voies restantes ; elle écorne le principe d'égalité d'accès à l'infrastructure publique ; et elle ne change rien au problème de fond, qui est qu'il y a trop de voitures pour la route disponible. La vraie solution reste la même qu'avant : élargir l'autoroute, ou convaincre une partie des gens de prendre le train.
|
||||||
|
|
||||||
|
### Le caveat 5G
|
||||||
|
|
||||||
|
Une nuance honnête pour finir. Le *network slicing* complique le débat juridique. Un opérateur peut créer des tranches de réseau avec des qualités différenciées en toute légalité quand il s'agit d'usages spécialisés — santé, industrie, transports. La question qui agite régulateurs et juristes depuis plusieurs années est de savoir où finit le service spécialisé légitime et où commence le contournement déguisé de la neutralité du net. L'ARCEP a ouvert ce chantier, et c'est probablement là, plus que dans une revanche commerciale brutale sur les forfaits premium, que se jouera la prochaine bataille.
|
||||||
|
|
||||||
|
Mais pour répondre simplement à la question : non, votre forfait à 50 € ne vous donne pas la priorité réseau sur celui de votre voisin à 10 €. Il vous donne plus de data, parfois un meilleur débit théorique, des options en plus. Pas une place dans la file.
|
||||||
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 151 KiB |
@@ -1,19 +1,17 @@
|
|||||||
À première vue, la situation a quelque chose de déroutant. Orange, opérateur historique français, héritier d’une longue tradition de service public et acteur stratégique des télécommunications, fait massivement appel aux infrastructures cloud d’Amazon, Microsoft ou Google. Dans un contexte où la souveraineté numérique est devenue un enjeu politique, économique et géostratégique majeur, ce choix peut apparaître comme une contradiction, voire comme un renoncement. Pourtant, il ne s’agit ni d’un abandon ni d’une naïveté, mais d’un compromis révélateur des limites actuelles de l’Europe dans la bataille du numérique.
|
Orange utilise massivement AWS, Azure et Google Cloud. Dit comme ça, c'est presque une blague. L'ancien France Télécom, opérateur historique, fleuron des télécoms français, héritier du service public, branché sur les serveurs de la Silicon Valley. À l'heure où on ne parle que de souveraineté numérique, on pourrait croire à une trahison. C'est plus compliqué que ça.
|
||||||
|
|
||||||
Le recours aux hyperscalers américains s’explique d’abord par une réalité industrielle. Ces entreprises ont construit, en une quinzaine d’années, une avance quasiment irréversible en matière d’infrastructures numériques. Leurs plateformes ne se contentent plus de fournir de la puissance de calcul ou du stockage. Elles offrent un écosystème complet qui va de l’hébergement à l’intelligence artificielle, en passant par l’analyse massive de données, l’automatisation des déploiements, la cybersécurité et la résilience globale des systèmes. Pour un groupe comme Orange, qui opère dans de nombreux pays et doit garantir des niveaux de service très élevés à des millions d’utilisateurs, cette maturité technologique est un facteur décisif.
|
La raison principale est bête : les Américains ont gagné la course. En quinze ans, AWS, Microsoft et Google ont construit une avance que personne ne sait combler aujourd'hui. Et ils ne vendent plus seulement du stockage ou de la puissance de calcul. Ils vendent un écosystème entier : de l'IA prête à l'emploi, des outils d'analyse de données, de l'automatisation, de la cybersécurité, des garanties de disponibilité à neuf chiffres. Pour Orange, qui doit faire tourner ses services dans une vingtaine de pays sans tomber en panne, ce niveau de maturité pèse lourd dans la balance.
|
||||||
|
|
||||||
Mais ce choix, rationnel sur le plan opérationnel, pose une question politique et stratégique beaucoup plus large. En confiant une part croissante de ses infrastructures numériques à des acteurs soumis au droit américain, Orange participe malgré lui à une forme de dépendance structurelle. Le Cloud Act, qui permet aux autorités américaines d’exiger l’accès à certaines données, même hébergées hors des États-Unis, symbolise cette vulnérabilité. Même si les mécanismes de chiffrement, de cloisonnement et de contrôle contractuel existent, le simple fait que la décision ultime puisse échapper aux juridictions européennes constitue une faille du point de vue de la souveraineté.
|
Sauf que ce choix rationnel a un prix politique. En confiant ses infrastructures à des entreprises soumises au droit américain, Orange entre dans une zone de dépendance dont on ne sort pas facilement. Le Cloud Act permet aux autorités américaines de réclamer des données hébergées par ces sociétés, même quand ces données sont physiquement en Europe. On peut chiffrer, cloisonner, négocier des clauses dans tous les sens, le fait reste que la décision finale échappe au juge européen. Pour un opérateur télécoms qui manipule des données de millions d'abonnés, ce n'est pas un détail.
|
||||||
|
|
||||||
Ce paradoxe est d’autant plus frappant que des alternatives européennes existent. La France et l’Europe disposent d’acteurs solides, capables de fournir des services cloud performants, sécurisés et compétitifs sur de nombreux usages. OVHcloud, Scaleway, Outscale, IONOS ou encore les initiatives portées par Deutsche Telekom en sont la preuve. Alors pourquoi ne pas construire une alliance massive entre ces acteurs et un groupe comme Orange pour bâtir une véritable alternative souveraine ?
|
Le plus rageant, c'est qu'on a des alternatives. OVHcloud, Scaleway, Outscale, IONOS en Allemagne, sans parler des projets autour de Deutsche Telekom. Ces acteurs existent, ils sont sérieux, ils savent faire. Alors pourquoi Orange ne s'allie pas avec eux pour construire quelque chose de crédible à l'échelle européenne ?
|
||||||
|
|
||||||
La réponse tient moins à la volonté qu’à l’échelle. Les hyperscalers américains investissent chaque année des dizaines de milliards d’euros dans leurs infrastructures. Ils déploient des data centers sur tous les continents, possèdent leurs propres réseaux mondiaux et attirent l’essentiel des talents du secteur. Face à cette puissance financière et industrielle, les acteurs européens, pris individuellement, peinent à rivaliser. Une alliance serait possible, mais elle nécessiterait un effort coordonné, soutenu politiquement, sur plusieurs décennies. Or l’Europe avance souvent par projets fragmentés, soumis aux cycles électoraux et aux priorités nationales parfois divergentes.
|
Parce que l'écart de moyens est vertigineux. AWS et Microsoft investissent chacun plus de cinquante milliards de dollars par an dans leurs infrastructures. Ils ont leurs propres câbles sous-marins, leurs propres réseaux mondiaux, et ils raflent une bonne partie des ingénieurs qui sortent des écoles. Un OVH, même bien géré, ne joue pas dans la même catégorie financière. Il faudrait une alliance européenne soutenue politiquement, financée sur vingt ou trente ans, pour espérer rattraper. On a essayé avec Gaia-X. Le résultat parle de lui-même.
|
||||||
|
|
||||||
Pour Orange, la situation se résume donc à un dilemme stratégique. S’en tenir strictement à des solutions européennes aujourd’hui impliquerait souvent de renoncer à certains niveaux de performance, de standardisation et de rapidité d’innovation. Dans un marché des télécoms déjà très concurrentiel, où les marges sont sous pression et où les attentes des clients sont toujours plus élevées, ce choix pourrait fragiliser l’entreprise à court et moyen terme. À l’inverse, s’appuyer sur les clouds américains permet de rester compétitif, mais au prix d’une dépendance qui pose problème à long terme.
|
Du coup, Orange est coincé. Tout miser sur l'européen aujourd'hui, ça veut dire accepter des services moins performants, moins riches, et perdre du terrain face à ses concurrents qui, eux, n'auront pas ces scrupules. Dans un marché où les marges fondent et où chaque innovation compte, c'est un pari risqué. Continuer avec les Américains, c'est rester dans la course mais accepter une dépendance qui peut, du jour au lendemain, devenir un problème géopolitique.
|
||||||
|
|
||||||
C’est dans cet entre-deux qu’émergent les stratégies hybrides. Orange, comme d’autres grands groupes européens, cherche à concilier deux impératifs contradictoires. D’un côté, tirer parti de la puissance des hyperscalers pour les usages nécessitant de la flexibilité, de l’innovation rapide et une échelle mondiale. De l’autre, développer des environnements de confiance pour les données sensibles, souvent en partenariat avec des acteurs européens, afin de garantir une maîtrise juridique et opérationnelle renforcée. Cette approche permet de limiter les risques sans renoncer totalement aux avantages technologiques des géants américains.
|
D'où la solution batarde que tout le monde adopte : l'hybride. On met chez Amazon ou Microsoft ce qui doit aller vite, innover, scaler. On garde en Europe, parfois sur des clouds "de confiance" labellisés SecNumCloud, ce qui touche aux données sensibles, aux clients régulés, à l'État. Ce n'est pas glorieux, mais ça permet de tenir les deux bouts.
|
||||||
|
|
||||||
Pour celles et ceux qui défendent activement la souveraineté numérique, ce compromis reste frustrant. Il donne l’impression d’un renoncement progressif, d’une Europe qui accepte de jouer sur un terrain défini par d’autres. Pourtant, le problème dépasse largement Orange. Il interroge la capacité collective du continent à se doter d’infrastructures numériques stratégiques, au même titre que l’énergie, la défense ou les transports. Tant que le cloud restera perçu uniquement comme un marché et non comme un enjeu de souveraineté, les décisions resteront dictées par des logiques de court terme.
|
Pour les défenseurs de la souveraineté numérique, ce compromis a un goût amer. On a l'impression d'une Europe qui se résigne, qui joue le match sur le terrain de l'adversaire avec ses règles. Mais en pointant Orange du doigt, on rate la cible. Le vrai problème n'est pas dans les choix d'une entreprise, il est en amont. Tant qu'on traitera le cloud comme un simple marché et pas comme une infrastructure critique, au même titre que l'électricité ou les chemins de fer, les industriels feront ce qu'ils ont toujours fait : choisir ce qui marche, là, maintenant.
|
||||||
|
|
||||||
La vraie question n’est donc pas de savoir pourquoi Orange utilise les clouds américains, mais pourquoi l’Europe n’a pas encore su créer un cadre suffisamment ambitieux pour rendre ce choix inutile. Tant que les alternatives souveraines resteront fragmentées, sous-financées ou cantonnées à des niches, les grands groupes continueront à faire des choix pragmatiques, même s’ils sont stratégiquement inconfortables. Défendre la souveraineté numérique, ce n’est pas seulement pointer les risques de dépendance, c’est aussi accepter que cette souveraineté a un coût, qu’elle nécessite des investissements lourds, une vision industrielle de long terme et une véritable volonté politique.
|
La bonne question n'est donc pas "pourquoi Orange utilise AWS". Elle est "pourquoi, vingt ans après l'arrivée du cloud, l'Europe n'a toujours pas mis sur la table de quoi rendre ce choix évitable". La souveraineté ne se décrète pas dans des communiqués. Elle se paie. En milliards, en années, en décisions politiques qui survivent aux changements de gouvernement. Tant qu'on ne sera pas prêts à ce niveau d'engagement, on continuera à tenir un discours sur l'indépendance numérique en signant des contrats avec Seattle et Redmond.
|
||||||
|
|
||||||
Dans ce contexte, le combat pour la souveraineté numérique n’est pas perdu, mais il reste à mener sur un autre terrain que celui des seules décisions d’entreprise. Il se joue au niveau des États, de l’Union européenne et des grandes orientations industrielles. Tant que ce cadre n’évoluera pas, même les acteurs les plus attachés à l’indépendance technologique continueront à avancer dans ce paradoxe : défendre la souveraineté dans le discours, tout en dépendant, dans les faits, des infrastructures des géants américains.
|
|
||||||
@@ -1,13 +1,149 @@
|
|||||||
{
|
{
|
||||||
"uuid": "75bf96ba-e110-4a9e-8163-95890562aecf",
|
"uuid": "75bf96ba-e110-4a9e-8163-95890562aecf",
|
||||||
"slug": "souverainete-numerique-le-paradoxe-d-orange-face-aux-clouds-americains",
|
"slug": "souverainete-numerique-le-paradoxe-d-orange-face-aux-clouds-americains",
|
||||||
"title": "Souveraineté numérique : le paradoxe d’Orange face aux clouds américains",
|
"title": "Orange dans les bras d'Amazon : l'aveu d'un échec européen",
|
||||||
"author": "cedric@abonnel.fr",
|
"author": "cedric@abonnel.fr",
|
||||||
"published": true,
|
"published": true,
|
||||||
"published_at": "2026-01-16 11:17:19",
|
"published_at": "2026-01-16 11:17",
|
||||||
"created_at": "2026-01-16 11:17:19",
|
"created_at": "2026-01-16 11:17:19",
|
||||||
"updated_at": "2026-01-16 11:17:19",
|
"updated_at": "2026-05-11 21:44:15",
|
||||||
"revisions": [],
|
"revisions": [
|
||||||
|
{
|
||||||
|
"n": 1,
|
||||||
|
"date": "2026-05-11 19:14:47",
|
||||||
|
"comment": "",
|
||||||
|
"title": "Souveraineté numérique : le paradoxe d’Orange face aux clouds américains"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"n": 2,
|
||||||
|
"date": "2026-05-11 19:16:11",
|
||||||
|
"comment": "",
|
||||||
|
"title": "Souveraineté numérique : le paradoxe d’Orange face aux clouds américains"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"n": 3,
|
||||||
|
"date": "2026-05-11 19:19:14",
|
||||||
|
"comment": "",
|
||||||
|
"title": "Souveraineté numérique : le paradoxe d’Orange face aux clouds américains"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"n": 4,
|
||||||
|
"date": "2026-05-11 19:23:36",
|
||||||
|
"comment": "",
|
||||||
|
"title": "Souveraineté numérique : le paradoxe d’Orange face aux clouds américains"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"n": 5,
|
||||||
|
"date": "2026-05-11 20:03:18",
|
||||||
|
"comment": "",
|
||||||
|
"title": "Souveraineté numérique : le paradoxe d’Orange face aux clouds américains"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"n": 6,
|
||||||
|
"date": "2026-05-11 20:04:17",
|
||||||
|
"comment": "",
|
||||||
|
"title": "Souveraineté numérique : le paradoxe d’Orange face aux clouds américains"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"n": 7,
|
||||||
|
"date": "2026-05-11 20:22:36",
|
||||||
|
"comment": "",
|
||||||
|
"title": "Souveraineté numérique : le paradoxe d’Orange face aux clouds américains"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"n": 8,
|
||||||
|
"date": "2026-05-11 20:23:25",
|
||||||
|
"comment": "",
|
||||||
|
"title": "Souveraineté numérique : le paradoxe d’Orange face aux clouds américains"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"n": 9,
|
||||||
|
"date": "2026-05-11 21:37:25",
|
||||||
|
"comment": "",
|
||||||
|
"title": "Orange dans les bras d'Amazon : l'aveu d'un échec européen"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"n": 10,
|
||||||
|
"date": "2026-05-11 21:38:02",
|
||||||
|
"comment": "",
|
||||||
|
"title": "Orange dans les bras d'Amazon : l'aveu d'un échec européen"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"n": 11,
|
||||||
|
"date": "2026-05-11 21:39:10",
|
||||||
|
"comment": "",
|
||||||
|
"title": "Orange dans les bras d'Amazon : l'aveu d'un échec européen"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"n": 12,
|
||||||
|
"date": "2026-05-11 21:40:24",
|
||||||
|
"comment": "",
|
||||||
|
"title": "Orange dans les bras d'Amazon : l'aveu d'un échec européen"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"n": 13,
|
||||||
|
"date": "2026-05-11 21:43:18",
|
||||||
|
"comment": "",
|
||||||
|
"title": "Orange dans les bras d'Amazon : l'aveu d'un échec européen"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"n": 14,
|
||||||
|
"date": "2026-05-11 21:44:15",
|
||||||
|
"comment": "",
|
||||||
|
"title": "Orange dans les bras d'Amazon : l'aveu d'un échec européen"
|
||||||
|
}
|
||||||
|
],
|
||||||
"cover": "cover.jpg",
|
"cover": "cover.jpg",
|
||||||
|
"files_meta": {
|
||||||
|
"cover.jpg": {
|
||||||
|
"author": "",
|
||||||
|
"source_url": "https://blog.wescale.fr/hubfs/CloudUS-Europe.jpg"
|
||||||
|
},
|
||||||
|
"l17cenum2526028_compte-rendu.pdf": {
|
||||||
|
"author": "",
|
||||||
|
"source_url": "https://www.assemblee-nationale.fr/dyn/17/comptes-rendus/cenum/l17cenum2526028_compte-rendu.pdf"
|
||||||
|
},
|
||||||
|
"l17cenum2526028_compte-rendu_1.pdf": {
|
||||||
|
"author": "",
|
||||||
|
"source_url": "https://www.assemblee-nationale.fr/dyn/17/comptes-rendus/cenum/l17cenum2526028_compte-rendu.pdf"
|
||||||
|
},
|
||||||
|
"Etude-Asteres-La-dependance-technologique-aux-services-de-cloud-et-logiciels-americains-avril-2025.pdf": {
|
||||||
|
"author": "",
|
||||||
|
"source_url": "https://www.cigref.fr/wp/wp-content/uploads/2025/04/Etude-Asteres-La-dependance-technologique-aux-services-de-cloud-et-logiciels-americains-avril-2025.pdf"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"external_links": [
|
||||||
|
{
|
||||||
|
"url": "https://www.lemondeinformatique.fr/les-dossiers/lire-cloud-souverain-une-interpretation-a-geometrie-variable-1678.html",
|
||||||
|
"name": "Cloud souverain, une interprétation à géométrie variable",
|
||||||
|
"added_at": "2026-05-11 21:40:20",
|
||||||
|
"meta": {
|
||||||
|
"mime": "text/html",
|
||||||
|
"size": 73778,
|
||||||
|
"description": "La bataille des clouds pour la souveraineté - Cloud souverain, une interprétation à géométrie variable - ",
|
||||||
|
"og_image": "/file?uuid=75bf96ba-e110-4a9e-8163-95890562aecf&name=_thumb_f44a2f694f44f2a5-11682.jpg",
|
||||||
|
"site_name": "LeMondeInformatique",
|
||||||
|
"og_type": "article",
|
||||||
|
"canonical": "https://www.lemondeinformatique.fr/les-dossiers/lire-cloud-souverain-une-interpretation-a-geometrie-variable-1678.html"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://www.assemblee-nationale.fr/dyn/17/comptes-rendus/cenum/l17cenum2526028_compte-rendu.pdf",
|
||||||
|
"name": "Commission d’enquête sur les dépendances structurelles et les vulnérabilités systémiques dans le secteur du numérique et les risques pour l’indépendance de la France",
|
||||||
|
"added_at": "2026-05-11 21:44:12",
|
||||||
|
"meta": {
|
||||||
|
"mime": "application/pdf",
|
||||||
|
"size": 659476,
|
||||||
|
"date": "2026-05-06 16:38:24+02:00",
|
||||||
|
"creator": "pdftk 2.02 - www.pdftk.com",
|
||||||
|
"producer": "itext-paulo-155 (itextpdf.sf.net-lowagie.com)",
|
||||||
|
"pages": 26,
|
||||||
|
"pdf_version": "PDF 1.7"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"seo_title": "",
|
||||||
|
"seo_description": "",
|
||||||
|
"og_image": "",
|
||||||
"category": "actualité"
|
"category": "actualité"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
À première vue, la situation a quelque chose de déroutant. Orange, opérateur historique français, héritier d’une longue tradition de service public et acteur stratégique des télécommunications, fait massivement appel aux infrastructures cloud d’Amazon, Microsoft ou Google. Dans un contexte où la souveraineté numérique est devenue un enjeu politique, économique et géostratégique majeur, ce choix peut apparaître comme une contradiction, voire comme un renoncement. Pourtant, il ne s’agit ni d’un abandon ni d’une naïveté, mais d’un compromis révélateur des limites actuelles de l’Europe dans la bataille du numérique.
|
||||||
|
|
||||||
|
Le recours aux hyperscalers américains s’explique d’abord par une réalité industrielle. Ces entreprises ont construit, en une quinzaine d’années, une avance quasiment irréversible en matière d’infrastructures numériques. Leurs plateformes ne se contentent plus de fournir de la puissance de calcul ou du stockage. Elles offrent un écosystème complet qui va de l’hébergement à l’intelligence artificielle, en passant par l’analyse massive de données, l’automatisation des déploiements, la cybersécurité et la résilience globale des systèmes. Pour un groupe comme Orange, qui opère dans de nombreux pays et doit garantir des niveaux de service très élevés à des millions d’utilisateurs, cette maturité technologique est un facteur décisif.
|
||||||
|
|
||||||
|
Mais ce choix, rationnel sur le plan opérationnel, pose une question politique et stratégique beaucoup plus large. En confiant une part croissante de ses infrastructures numériques à des acteurs soumis au droit américain, Orange participe malgré lui à une forme de dépendance structurelle. Le Cloud Act, qui permet aux autorités américaines d’exiger l’accès à certaines données, même hébergées hors des États-Unis, symbolise cette vulnérabilité. Même si les mécanismes de chiffrement, de cloisonnement et de contrôle contractuel existent, le simple fait que la décision ultime puisse échapper aux juridictions européennes constitue une faille du point de vue de la souveraineté.
|
||||||
|
|
||||||
|
Ce paradoxe est d’autant plus frappant que des alternatives européennes existent. La France et l’Europe disposent d’acteurs solides, capables de fournir des services cloud performants, sécurisés et compétitifs sur de nombreux usages. OVHcloud, Scaleway, Outscale, IONOS ou encore les initiatives portées par Deutsche Telekom en sont la preuve. Alors pourquoi ne pas construire une alliance massive entre ces acteurs et un groupe comme Orange pour bâtir une véritable alternative souveraine ?
|
||||||
|
|
||||||
|
La réponse tient moins à la volonté qu’à l’échelle. Les hyperscalers américains investissent chaque année des dizaines de milliards d’euros dans leurs infrastructures. Ils déploient des data centers sur tous les continents, possèdent leurs propres réseaux mondiaux et attirent l’essentiel des talents du secteur. Face à cette puissance financière et industrielle, les acteurs européens, pris individuellement, peinent à rivaliser. Une alliance serait possible, mais elle nécessiterait un effort coordonné, soutenu politiquement, sur plusieurs décennies. Or l’Europe avance souvent par projets fragmentés, soumis aux cycles électoraux et aux priorités nationales parfois divergentes.
|
||||||
|
|
||||||
|
Pour Orange, la situation se résume donc à un dilemme stratégique. S’en tenir strictement à des solutions européennes aujourd’hui impliquerait souvent de renoncer à certains niveaux de performance, de standardisation et de rapidité d’innovation. Dans un marché des télécoms déjà très concurrentiel, où les marges sont sous pression et où les attentes des clients sont toujours plus élevées, ce choix pourrait fragiliser l’entreprise à court et moyen terme. À l’inverse, s’appuyer sur les clouds américains permet de rester compétitif, mais au prix d’une dépendance qui pose problème à long terme.
|
||||||
|
|
||||||
|
C’est dans cet entre-deux qu’émergent les stratégies hybrides. Orange, comme d’autres grands groupes européens, cherche à concilier deux impératifs contradictoires. D’un côté, tirer parti de la puissance des hyperscalers pour les usages nécessitant de la flexibilité, de l’innovation rapide et une échelle mondiale. De l’autre, développer des environnements de confiance pour les données sensibles, souvent en partenariat avec des acteurs européens, afin de garantir une maîtrise juridique et opérationnelle renforcée. Cette approche permet de limiter les risques sans renoncer totalement aux avantages technologiques des géants américains.
|
||||||
|
|
||||||
|
Pour celles et ceux qui défendent activement la souveraineté numérique, ce compromis reste frustrant. Il donne l’impression d’un renoncement progressif, d’une Europe qui accepte de jouer sur un terrain défini par d’autres. Pourtant, le problème dépasse largement Orange. Il interroge la capacité collective du continent à se doter d’infrastructures numériques stratégiques, au même titre que l’énergie, la défense ou les transports. Tant que le cloud restera perçu uniquement comme un marché et non comme un enjeu de souveraineté, les décisions resteront dictées par des logiques de court terme.
|
||||||
|
|
||||||
|
La vraie question n’est donc pas de savoir pourquoi Orange utilise les clouds américains, mais pourquoi l’Europe n’a pas encore su créer un cadre suffisamment ambitieux pour rendre ce choix inutile. Tant que les alternatives souveraines resteront fragmentées, sous-financées ou cantonnées à des niches, les grands groupes continueront à faire des choix pragmatiques, même s’ils sont stratégiquement inconfortables. Défendre la souveraineté numérique, ce n’est pas seulement pointer les risques de dépendance, c’est aussi accepter que cette souveraineté a un coût, qu’elle nécessite des investissements lourds, une vision industrielle de long terme et une véritable volonté politique.
|
||||||
|
|
||||||
|
Dans ce contexte, le combat pour la souveraineté numérique n’est pas perdu, mais il reste à mener sur un autre terrain que celui des seules décisions d’entreprise. Il se joue au niveau des États, de l’Union européenne et des grandes orientations industrielles. Tant que ce cadre n’évoluera pas, même les acteurs les plus attachés à l’indépendance technologique continueront à avancer dans ce paradoxe : défendre la souveraineté dans le discours, tout en dépendant, dans les faits, des infrastructures des géants américains.
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
À première vue, la situation a quelque chose de déroutant. Orange, opérateur historique français, héritier d’une longue tradition de service public et acteur stratégique des télécommunications, fait massivement appel aux infrastructures cloud d’Amazon, Microsoft ou Google. Dans un contexte où la souveraineté numérique est devenue un enjeu politique, économique et géostratégique majeur, ce choix peut apparaître comme une contradiction, voire comme un renoncement. Pourtant, il ne s’agit ni d’un abandon ni d’une naïveté, mais d’un compromis révélateur des limites actuelles de l’Europe dans la bataille du numérique.
|
||||||
|
|
||||||
|
Le recours aux hyperscalers américains s’explique d’abord par une réalité industrielle. Ces entreprises ont construit, en une quinzaine d’années, une avance quasiment irréversible en matière d’infrastructures numériques. Leurs plateformes ne se contentent plus de fournir de la puissance de calcul ou du stockage. Elles offrent un écosystème complet qui va de l’hébergement à l’intelligence artificielle, en passant par l’analyse massive de données, l’automatisation des déploiements, la cybersécurité et la résilience globale des systèmes. Pour un groupe comme Orange, qui opère dans de nombreux pays et doit garantir des niveaux de service très élevés à des millions d’utilisateurs, cette maturité technologique est un facteur décisif.
|
||||||
|
|
||||||
|
Mais ce choix, rationnel sur le plan opérationnel, pose une question politique et stratégique beaucoup plus large. En confiant une part croissante de ses infrastructures numériques à des acteurs soumis au droit américain, Orange participe malgré lui à une forme de dépendance structurelle. Le Cloud Act, qui permet aux autorités américaines d’exiger l’accès à certaines données, même hébergées hors des États-Unis, symbolise cette vulnérabilité. Même si les mécanismes de chiffrement, de cloisonnement et de contrôle contractuel existent, le simple fait que la décision ultime puisse échapper aux juridictions européennes constitue une faille du point de vue de la souveraineté.
|
||||||
|
|
||||||
|
Ce paradoxe est d’autant plus frappant que des alternatives européennes existent. La France et l’Europe disposent d’acteurs solides, capables de fournir des services cloud performants, sécurisés et compétitifs sur de nombreux usages. OVHcloud, Scaleway, Outscale, IONOS ou encore les initiatives portées par Deutsche Telekom en sont la preuve. Alors pourquoi ne pas construire une alliance massive entre ces acteurs et un groupe comme Orange pour bâtir une véritable alternative souveraine ?
|
||||||
|
|
||||||
|
La réponse tient moins à la volonté qu’à l’échelle. Les hyperscalers américains investissent chaque année des dizaines de milliards d’euros dans leurs infrastructures. Ils déploient des data centers sur tous les continents, possèdent leurs propres réseaux mondiaux et attirent l’essentiel des talents du secteur. Face à cette puissance financière et industrielle, les acteurs européens, pris individuellement, peinent à rivaliser. Une alliance serait possible, mais elle nécessiterait un effort coordonné, soutenu politiquement, sur plusieurs décennies. Or l’Europe avance souvent par projets fragmentés, soumis aux cycles électoraux et aux priorités nationales parfois divergentes.
|
||||||
|
|
||||||
|
Pour Orange, la situation se résume donc à un dilemme stratégique. S’en tenir strictement à des solutions européennes aujourd’hui impliquerait souvent de renoncer à certains niveaux de performance, de standardisation et de rapidité d’innovation. Dans un marché des télécoms déjà très concurrentiel, où les marges sont sous pression et où les attentes des clients sont toujours plus élevées, ce choix pourrait fragiliser l’entreprise à court et moyen terme. À l’inverse, s’appuyer sur les clouds américains permet de rester compétitif, mais au prix d’une dépendance qui pose problème à long terme.
|
||||||
|
|
||||||
|
C’est dans cet entre-deux qu’émergent les stratégies hybrides. Orange, comme d’autres grands groupes européens, cherche à concilier deux impératifs contradictoires. D’un côté, tirer parti de la puissance des hyperscalers pour les usages nécessitant de la flexibilité, de l’innovation rapide et une échelle mondiale. De l’autre, développer des environnements de confiance pour les données sensibles, souvent en partenariat avec des acteurs européens, afin de garantir une maîtrise juridique et opérationnelle renforcée. Cette approche permet de limiter les risques sans renoncer totalement aux avantages technologiques des géants américains.
|
||||||
|
|
||||||
|
Pour celles et ceux qui défendent activement la souveraineté numérique, ce compromis reste frustrant. Il donne l’impression d’un renoncement progressif, d’une Europe qui accepte de jouer sur un terrain défini par d’autres. Pourtant, le problème dépasse largement Orange. Il interroge la capacité collective du continent à se doter d’infrastructures numériques stratégiques, au même titre que l’énergie, la défense ou les transports. Tant que le cloud restera perçu uniquement comme un marché et non comme un enjeu de souveraineté, les décisions resteront dictées par des logiques de court terme.
|
||||||
|
|
||||||
|
La vraie question n’est donc pas de savoir pourquoi Orange utilise les clouds américains, mais pourquoi l’Europe n’a pas encore su créer un cadre suffisamment ambitieux pour rendre ce choix inutile. Tant que les alternatives souveraines resteront fragmentées, sous-financées ou cantonnées à des niches, les grands groupes continueront à faire des choix pragmatiques, même s’ils sont stratégiquement inconfortables. Défendre la souveraineté numérique, ce n’est pas seulement pointer les risques de dépendance, c’est aussi accepter que cette souveraineté a un coût, qu’elle nécessite des investissements lourds, une vision industrielle de long terme et une véritable volonté politique.
|
||||||
|
|
||||||
|
Dans ce contexte, le combat pour la souveraineté numérique n’est pas perdu, mais il reste à mener sur un autre terrain que celui des seules décisions d’entreprise. Il se joue au niveau des États, de l’Union européenne et des grandes orientations industrielles. Tant que ce cadre n’évoluera pas, même les acteurs les plus attachés à l’indépendance technologique continueront à avancer dans ce paradoxe : défendre la souveraineté dans le discours, tout en dépendant, dans les faits, des infrastructures des géants américains.
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
À première vue, la situation a quelque chose de déroutant. Orange, opérateur historique français, héritier d’une longue tradition de service public et acteur stratégique des télécommunications, fait massivement appel aux infrastructures cloud d’Amazon, Microsoft ou Google. Dans un contexte où la souveraineté numérique est devenue un enjeu politique, économique et géostratégique majeur, ce choix peut apparaître comme une contradiction, voire comme un renoncement. Pourtant, il ne s’agit ni d’un abandon ni d’une naïveté, mais d’un compromis révélateur des limites actuelles de l’Europe dans la bataille du numérique.
|
||||||
|
|
||||||
|
Le recours aux hyperscalers américains s’explique d’abord par une réalité industrielle. Ces entreprises ont construit, en une quinzaine d’années, une avance quasiment irréversible en matière d’infrastructures numériques. Leurs plateformes ne se contentent plus de fournir de la puissance de calcul ou du stockage. Elles offrent un écosystème complet qui va de l’hébergement à l’intelligence artificielle, en passant par l’analyse massive de données, l’automatisation des déploiements, la cybersécurité et la résilience globale des systèmes. Pour un groupe comme Orange, qui opère dans de nombreux pays et doit garantir des niveaux de service très élevés à des millions d’utilisateurs, cette maturité technologique est un facteur décisif.
|
||||||
|
|
||||||
|
Mais ce choix, rationnel sur le plan opérationnel, pose une question politique et stratégique beaucoup plus large. En confiant une part croissante de ses infrastructures numériques à des acteurs soumis au droit américain, Orange participe malgré lui à une forme de dépendance structurelle. Le Cloud Act, qui permet aux autorités américaines d’exiger l’accès à certaines données, même hébergées hors des États-Unis, symbolise cette vulnérabilité. Même si les mécanismes de chiffrement, de cloisonnement et de contrôle contractuel existent, le simple fait que la décision ultime puisse échapper aux juridictions européennes constitue une faille du point de vue de la souveraineté.
|
||||||
|
|
||||||
|
Ce paradoxe est d’autant plus frappant que des alternatives européennes existent. La France et l’Europe disposent d’acteurs solides, capables de fournir des services cloud performants, sécurisés et compétitifs sur de nombreux usages. OVHcloud, Scaleway, Outscale, IONOS ou encore les initiatives portées par Deutsche Telekom en sont la preuve. Alors pourquoi ne pas construire une alliance massive entre ces acteurs et un groupe comme Orange pour bâtir une véritable alternative souveraine ?
|
||||||
|
|
||||||
|
La réponse tient moins à la volonté qu’à l’échelle. Les hyperscalers américains investissent chaque année des dizaines de milliards d’euros dans leurs infrastructures. Ils déploient des data centers sur tous les continents, possèdent leurs propres réseaux mondiaux et attirent l’essentiel des talents du secteur. Face à cette puissance financière et industrielle, les acteurs européens, pris individuellement, peinent à rivaliser. Une alliance serait possible, mais elle nécessiterait un effort coordonné, soutenu politiquement, sur plusieurs décennies. Or l’Europe avance souvent par projets fragmentés, soumis aux cycles électoraux et aux priorités nationales parfois divergentes.
|
||||||
|
|
||||||
|
Pour Orange, la situation se résume donc à un dilemme stratégique. S’en tenir strictement à des solutions européennes aujourd’hui impliquerait souvent de renoncer à certains niveaux de performance, de standardisation et de rapidité d’innovation. Dans un marché des télécoms déjà très concurrentiel, où les marges sont sous pression et où les attentes des clients sont toujours plus élevées, ce choix pourrait fragiliser l’entreprise à court et moyen terme. À l’inverse, s’appuyer sur les clouds américains permet de rester compétitif, mais au prix d’une dépendance qui pose problème à long terme.
|
||||||
|
|
||||||
|
C’est dans cet entre-deux qu’émergent les stratégies hybrides. Orange, comme d’autres grands groupes européens, cherche à concilier deux impératifs contradictoires. D’un côté, tirer parti de la puissance des hyperscalers pour les usages nécessitant de la flexibilité, de l’innovation rapide et une échelle mondiale. De l’autre, développer des environnements de confiance pour les données sensibles, souvent en partenariat avec des acteurs européens, afin de garantir une maîtrise juridique et opérationnelle renforcée. Cette approche permet de limiter les risques sans renoncer totalement aux avantages technologiques des géants américains.
|
||||||
|
|
||||||
|
Pour celles et ceux qui défendent activement la souveraineté numérique, ce compromis reste frustrant. Il donne l’impression d’un renoncement progressif, d’une Europe qui accepte de jouer sur un terrain défini par d’autres. Pourtant, le problème dépasse largement Orange. Il interroge la capacité collective du continent à se doter d’infrastructures numériques stratégiques, au même titre que l’énergie, la défense ou les transports. Tant que le cloud restera perçu uniquement comme un marché et non comme un enjeu de souveraineté, les décisions resteront dictées par des logiques de court terme.
|
||||||
|
|
||||||
|
La vraie question n’est donc pas de savoir pourquoi Orange utilise les clouds américains, mais pourquoi l’Europe n’a pas encore su créer un cadre suffisamment ambitieux pour rendre ce choix inutile. Tant que les alternatives souveraines resteront fragmentées, sous-financées ou cantonnées à des niches, les grands groupes continueront à faire des choix pragmatiques, même s’ils sont stratégiquement inconfortables. Défendre la souveraineté numérique, ce n’est pas seulement pointer les risques de dépendance, c’est aussi accepter que cette souveraineté a un coût, qu’elle nécessite des investissements lourds, une vision industrielle de long terme et une véritable volonté politique.
|
||||||
|
|
||||||
|
Dans ce contexte, le combat pour la souveraineté numérique n’est pas perdu, mais il reste à mener sur un autre terrain que celui des seules décisions d’entreprise. Il se joue au niveau des États, de l’Union européenne et des grandes orientations industrielles. Tant que ce cadre n’évoluera pas, même les acteurs les plus attachés à l’indépendance technologique continueront à avancer dans ce paradoxe : défendre la souveraineté dans le discours, tout en dépendant, dans les faits, des infrastructures des géants américains.
|
||||||
|
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
À première vue, la situation a quelque chose de déroutant. Orange, opérateur historique français, héritier d’une longue tradition de service public et acteur stratégique des télécommunications, fait massivement appel aux infrastructures cloud d’Amazon, Microsoft ou Google. Dans un contexte où la souveraineté numérique est devenue un enjeu politique, économique et géostratégique majeur, ce choix peut apparaître comme une contradiction, voire comme un renoncement. Pourtant, il ne s’agit ni d’un abandon ni d’une naïveté, mais d’un compromis révélateur des limites actuelles de l’Europe dans la bataille du numérique.
|
||||||
|
|
||||||
|
Le recours aux hyperscalers américains s’explique d’abord par une réalité industrielle. Ces entreprises ont construit, en une quinzaine d’années, une avance quasiment irréversible en matière d’infrastructures numériques. Leurs plateformes ne se contentent plus de fournir de la puissance de calcul ou du stockage. Elles offrent un écosystème complet qui va de l’hébergement à l’intelligence artificielle, en passant par l’analyse massive de données, l’automatisation des déploiements, la cybersécurité et la résilience globale des systèmes. Pour un groupe comme Orange, qui opère dans de nombreux pays et doit garantir des niveaux de service très élevés à des millions d’utilisateurs, cette maturité technologique est un facteur décisif.
|
||||||
|
|
||||||
|
Mais ce choix, rationnel sur le plan opérationnel, pose une question politique et stratégique beaucoup plus large. En confiant une part croissante de ses infrastructures numériques à des acteurs soumis au droit américain, Orange participe malgré lui à une forme de dépendance structurelle. Le Cloud Act, qui permet aux autorités américaines d’exiger l’accès à certaines données, même hébergées hors des États-Unis, symbolise cette vulnérabilité. Même si les mécanismes de chiffrement, de cloisonnement et de contrôle contractuel existent, le simple fait que la décision ultime puisse échapper aux juridictions européennes constitue une faille du point de vue de la souveraineté.
|
||||||
|
|
||||||
|
Ce paradoxe est d’autant plus frappant que des alternatives européennes existent. La France et l’Europe disposent d’acteurs solides, capables de fournir des services cloud performants, sécurisés et compétitifs sur de nombreux usages. OVHcloud, Scaleway, Outscale, IONOS ou encore les initiatives portées par Deutsche Telekom en sont la preuve. Alors pourquoi ne pas construire une alliance massive entre ces acteurs et un groupe comme Orange pour bâtir une véritable alternative souveraine ?
|
||||||
|
|
||||||
|
La réponse tient moins à la volonté qu’à l’échelle. Les hyperscalers américains investissent chaque année des dizaines de milliards d’euros dans leurs infrastructures. Ils déploient des data centers sur tous les continents, possèdent leurs propres réseaux mondiaux et attirent l’essentiel des talents du secteur. Face à cette puissance financière et industrielle, les acteurs européens, pris individuellement, peinent à rivaliser. Une alliance serait possible, mais elle nécessiterait un effort coordonné, soutenu politiquement, sur plusieurs décennies. Or l’Europe avance souvent par projets fragmentés, soumis aux cycles électoraux et aux priorités nationales parfois divergentes.
|
||||||
|
|
||||||
|
Pour Orange, la situation se résume donc à un dilemme stratégique. S’en tenir strictement à des solutions européennes aujourd’hui impliquerait souvent de renoncer à certains niveaux de performance, de standardisation et de rapidité d’innovation. Dans un marché des télécoms déjà très concurrentiel, où les marges sont sous pression et où les attentes des clients sont toujours plus élevées, ce choix pourrait fragiliser l’entreprise à court et moyen terme. À l’inverse, s’appuyer sur les clouds américains permet de rester compétitif, mais au prix d’une dépendance qui pose problème à long terme.
|
||||||
|
|
||||||
|
C’est dans cet entre-deux qu’émergent les stratégies hybrides. Orange, comme d’autres grands groupes européens, cherche à concilier deux impératifs contradictoires. D’un côté, tirer parti de la puissance des hyperscalers pour les usages nécessitant de la flexibilité, de l’innovation rapide et une échelle mondiale. De l’autre, développer des environnements de confiance pour les données sensibles, souvent en partenariat avec des acteurs européens, afin de garantir une maîtrise juridique et opérationnelle renforcée. Cette approche permet de limiter les risques sans renoncer totalement aux avantages technologiques des géants américains.
|
||||||
|
|
||||||
|
Pour celles et ceux qui défendent activement la souveraineté numérique, ce compromis reste frustrant. Il donne l’impression d’un renoncement progressif, d’une Europe qui accepte de jouer sur un terrain défini par d’autres. Pourtant, le problème dépasse largement Orange. Il interroge la capacité collective du continent à se doter d’infrastructures numériques stratégiques, au même titre que l’énergie, la défense ou les transports. Tant que le cloud restera perçu uniquement comme un marché et non comme un enjeu de souveraineté, les décisions resteront dictées par des logiques de court terme.
|
||||||
|
|
||||||
|
La vraie question n’est donc pas de savoir pourquoi Orange utilise les clouds américains, mais pourquoi l’Europe n’a pas encore su créer un cadre suffisamment ambitieux pour rendre ce choix inutile. Tant que les alternatives souveraines resteront fragmentées, sous-financées ou cantonnées à des niches, les grands groupes continueront à faire des choix pragmatiques, même s’ils sont stratégiquement inconfortables. Défendre la souveraineté numérique, ce n’est pas seulement pointer les risques de dépendance, c’est aussi accepter que cette souveraineté a un coût, qu’elle nécessite des investissements lourds, une vision industrielle de long terme et une véritable volonté politique.
|
||||||
|
|
||||||
|
Dans ce contexte, le combat pour la souveraineté numérique n’est pas perdu, mais il reste à mener sur un autre terrain que celui des seules décisions d’entreprise. Il se joue au niveau des États, de l’Union européenne et des grandes orientations industrielles. Tant que ce cadre n’évoluera pas, même les acteurs les plus attachés à l’indépendance technologique continueront à avancer dans ce paradoxe : défendre la souveraineté dans le discours, tout en dépendant, dans les faits, des infrastructures des géants américains.
|
||||||
|
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
À première vue, la situation a quelque chose de déroutant. Orange, opérateur historique français, héritier d’une longue tradition de service public et acteur stratégique des télécommunications, fait massivement appel aux infrastructures cloud d’Amazon, Microsoft ou Google. Dans un contexte où la souveraineté numérique est devenue un enjeu politique, économique et géostratégique majeur, ce choix peut apparaître comme une contradiction, voire comme un renoncement. Pourtant, il ne s’agit ni d’un abandon ni d’une naïveté, mais d’un compromis révélateur des limites actuelles de l’Europe dans la bataille du numérique.
|
||||||
|
|
||||||
|
Le recours aux hyperscalers américains s’explique d’abord par une réalité industrielle. Ces entreprises ont construit, en une quinzaine d’années, une avance quasiment irréversible en matière d’infrastructures numériques. Leurs plateformes ne se contentent plus de fournir de la puissance de calcul ou du stockage. Elles offrent un écosystème complet qui va de l’hébergement à l’intelligence artificielle, en passant par l’analyse massive de données, l’automatisation des déploiements, la cybersécurité et la résilience globale des systèmes. Pour un groupe comme Orange, qui opère dans de nombreux pays et doit garantir des niveaux de service très élevés à des millions d’utilisateurs, cette maturité technologique est un facteur décisif.
|
||||||
|
|
||||||
|
Mais ce choix, rationnel sur le plan opérationnel, pose une question politique et stratégique beaucoup plus large. En confiant une part croissante de ses infrastructures numériques à des acteurs soumis au droit américain, Orange participe malgré lui à une forme de dépendance structurelle. Le Cloud Act, qui permet aux autorités américaines d’exiger l’accès à certaines données, même hébergées hors des États-Unis, symbolise cette vulnérabilité. Même si les mécanismes de chiffrement, de cloisonnement et de contrôle contractuel existent, le simple fait que la décision ultime puisse échapper aux juridictions européennes constitue une faille du point de vue de la souveraineté.
|
||||||
|
|
||||||
|
Ce paradoxe est d’autant plus frappant que des alternatives européennes existent. La France et l’Europe disposent d’acteurs solides, capables de fournir des services cloud performants, sécurisés et compétitifs sur de nombreux usages. OVHcloud, Scaleway, Outscale, IONOS ou encore les initiatives portées par Deutsche Telekom en sont la preuve. Alors pourquoi ne pas construire une alliance massive entre ces acteurs et un groupe comme Orange pour bâtir une véritable alternative souveraine ?
|
||||||
|
|
||||||
|
La réponse tient moins à la volonté qu’à l’échelle. Les hyperscalers américains investissent chaque année des dizaines de milliards d’euros dans leurs infrastructures. Ils déploient des data centers sur tous les continents, possèdent leurs propres réseaux mondiaux et attirent l’essentiel des talents du secteur. Face à cette puissance financière et industrielle, les acteurs européens, pris individuellement, peinent à rivaliser. Une alliance serait possible, mais elle nécessiterait un effort coordonné, soutenu politiquement, sur plusieurs décennies. Or l’Europe avance souvent par projets fragmentés, soumis aux cycles électoraux et aux priorités nationales parfois divergentes.
|
||||||
|
|
||||||
|
Pour Orange, la situation se résume donc à un dilemme stratégique. S’en tenir strictement à des solutions européennes aujourd’hui impliquerait souvent de renoncer à certains niveaux de performance, de standardisation et de rapidité d’innovation. Dans un marché des télécoms déjà très concurrentiel, où les marges sont sous pression et où les attentes des clients sont toujours plus élevées, ce choix pourrait fragiliser l’entreprise à court et moyen terme. À l’inverse, s’appuyer sur les clouds américains permet de rester compétitif, mais au prix d’une dépendance qui pose problème à long terme.
|
||||||
|
|
||||||
|
C’est dans cet entre-deux qu’émergent les stratégies hybrides. Orange, comme d’autres grands groupes européens, cherche à concilier deux impératifs contradictoires. D’un côté, tirer parti de la puissance des hyperscalers pour les usages nécessitant de la flexibilité, de l’innovation rapide et une échelle mondiale. De l’autre, développer des environnements de confiance pour les données sensibles, souvent en partenariat avec des acteurs européens, afin de garantir une maîtrise juridique et opérationnelle renforcée. Cette approche permet de limiter les risques sans renoncer totalement aux avantages technologiques des géants américains.
|
||||||
|
|
||||||
|
Pour celles et ceux qui défendent activement la souveraineté numérique, ce compromis reste frustrant. Il donne l’impression d’un renoncement progressif, d’une Europe qui accepte de jouer sur un terrain défini par d’autres. Pourtant, le problème dépasse largement Orange. Il interroge la capacité collective du continent à se doter d’infrastructures numériques stratégiques, au même titre que l’énergie, la défense ou les transports. Tant que le cloud restera perçu uniquement comme un marché et non comme un enjeu de souveraineté, les décisions resteront dictées par des logiques de court terme.
|
||||||
|
|
||||||
|
La vraie question n’est donc pas de savoir pourquoi Orange utilise les clouds américains, mais pourquoi l’Europe n’a pas encore su créer un cadre suffisamment ambitieux pour rendre ce choix inutile. Tant que les alternatives souveraines resteront fragmentées, sous-financées ou cantonnées à des niches, les grands groupes continueront à faire des choix pragmatiques, même s’ils sont stratégiquement inconfortables. Défendre la souveraineté numérique, ce n’est pas seulement pointer les risques de dépendance, c’est aussi accepter que cette souveraineté a un coût, qu’elle nécessite des investissements lourds, une vision industrielle de long terme et une véritable volonté politique.
|
||||||
|
|
||||||
|
Dans ce contexte, le combat pour la souveraineté numérique n’est pas perdu, mais il reste à mener sur un autre terrain que celui des seules décisions d’entreprise. Il se joue au niveau des États, de l’Union européenne et des grandes orientations industrielles. Tant que ce cadre n’évoluera pas, même les acteurs les plus attachés à l’indépendance technologique continueront à avancer dans ce paradoxe : défendre la souveraineté dans le discours, tout en dépendant, dans les faits, des infrastructures des géants américains.
|
||||||
|
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
À première vue, la situation a quelque chose de déroutant. Orange, opérateur historique français, héritier d’une longue tradition de service public et acteur stratégique des télécommunications, fait massivement appel aux infrastructures cloud d’Amazon, Microsoft ou Google. Dans un contexte où la souveraineté numérique est devenue un enjeu politique, économique et géostratégique majeur, ce choix peut apparaître comme une contradiction, voire comme un renoncement. Pourtant, il ne s’agit ni d’un abandon ni d’une naïveté, mais d’un compromis révélateur des limites actuelles de l’Europe dans la bataille du numérique.
|
||||||
|
|
||||||
|
Le recours aux hyperscalers américains s’explique d’abord par une réalité industrielle. Ces entreprises ont construit, en une quinzaine d’années, une avance quasiment irréversible en matière d’infrastructures numériques. Leurs plateformes ne se contentent plus de fournir de la puissance de calcul ou du stockage. Elles offrent un écosystème complet qui va de l’hébergement à l’intelligence artificielle, en passant par l’analyse massive de données, l’automatisation des déploiements, la cybersécurité et la résilience globale des systèmes. Pour un groupe comme Orange, qui opère dans de nombreux pays et doit garantir des niveaux de service très élevés à des millions d’utilisateurs, cette maturité technologique est un facteur décisif.
|
||||||
|
|
||||||
|
Mais ce choix, rationnel sur le plan opérationnel, pose une question politique et stratégique beaucoup plus large. En confiant une part croissante de ses infrastructures numériques à des acteurs soumis au droit américain, Orange participe malgré lui à une forme de dépendance structurelle. Le Cloud Act, qui permet aux autorités américaines d’exiger l’accès à certaines données, même hébergées hors des États-Unis, symbolise cette vulnérabilité. Même si les mécanismes de chiffrement, de cloisonnement et de contrôle contractuel existent, le simple fait que la décision ultime puisse échapper aux juridictions européennes constitue une faille du point de vue de la souveraineté.
|
||||||
|
|
||||||
|
Ce paradoxe est d’autant plus frappant que des alternatives européennes existent. La France et l’Europe disposent d’acteurs solides, capables de fournir des services cloud performants, sécurisés et compétitifs sur de nombreux usages. OVHcloud, Scaleway, Outscale, IONOS ou encore les initiatives portées par Deutsche Telekom en sont la preuve. Alors pourquoi ne pas construire une alliance massive entre ces acteurs et un groupe comme Orange pour bâtir une véritable alternative souveraine ?
|
||||||
|
|
||||||
|
La réponse tient moins à la volonté qu’à l’échelle. Les hyperscalers américains investissent chaque année des dizaines de milliards d’euros dans leurs infrastructures. Ils déploient des data centers sur tous les continents, possèdent leurs propres réseaux mondiaux et attirent l’essentiel des talents du secteur. Face à cette puissance financière et industrielle, les acteurs européens, pris individuellement, peinent à rivaliser. Une alliance serait possible, mais elle nécessiterait un effort coordonné, soutenu politiquement, sur plusieurs décennies. Or l’Europe avance souvent par projets fragmentés, soumis aux cycles électoraux et aux priorités nationales parfois divergentes.
|
||||||
|
|
||||||
|
Pour Orange, la situation se résume donc à un dilemme stratégique. S’en tenir strictement à des solutions européennes aujourd’hui impliquerait souvent de renoncer à certains niveaux de performance, de standardisation et de rapidité d’innovation. Dans un marché des télécoms déjà très concurrentiel, où les marges sont sous pression et où les attentes des clients sont toujours plus élevées, ce choix pourrait fragiliser l’entreprise à court et moyen terme. À l’inverse, s’appuyer sur les clouds américains permet de rester compétitif, mais au prix d’une dépendance qui pose problème à long terme.
|
||||||
|
|
||||||
|
C’est dans cet entre-deux qu’émergent les stratégies hybrides. Orange, comme d’autres grands groupes européens, cherche à concilier deux impératifs contradictoires. D’un côté, tirer parti de la puissance des hyperscalers pour les usages nécessitant de la flexibilité, de l’innovation rapide et une échelle mondiale. De l’autre, développer des environnements de confiance pour les données sensibles, souvent en partenariat avec des acteurs européens, afin de garantir une maîtrise juridique et opérationnelle renforcée. Cette approche permet de limiter les risques sans renoncer totalement aux avantages technologiques des géants américains.
|
||||||
|
|
||||||
|
Pour celles et ceux qui défendent activement la souveraineté numérique, ce compromis reste frustrant. Il donne l’impression d’un renoncement progressif, d’une Europe qui accepte de jouer sur un terrain défini par d’autres. Pourtant, le problème dépasse largement Orange. Il interroge la capacité collective du continent à se doter d’infrastructures numériques stratégiques, au même titre que l’énergie, la défense ou les transports. Tant que le cloud restera perçu uniquement comme un marché et non comme un enjeu de souveraineté, les décisions resteront dictées par des logiques de court terme.
|
||||||
|
|
||||||
|
La vraie question n’est donc pas de savoir pourquoi Orange utilise les clouds américains, mais pourquoi l’Europe n’a pas encore su créer un cadre suffisamment ambitieux pour rendre ce choix inutile. Tant que les alternatives souveraines resteront fragmentées, sous-financées ou cantonnées à des niches, les grands groupes continueront à faire des choix pragmatiques, même s’ils sont stratégiquement inconfortables. Défendre la souveraineté numérique, ce n’est pas seulement pointer les risques de dépendance, c’est aussi accepter que cette souveraineté a un coût, qu’elle nécessite des investissements lourds, une vision industrielle de long terme et une véritable volonté politique.
|
||||||
|
|
||||||
|
Dans ce contexte, le combat pour la souveraineté numérique n’est pas perdu, mais il reste à mener sur un autre terrain que celui des seules décisions d’entreprise. Il se joue au niveau des États, de l’Union européenne et des grandes orientations industrielles. Tant que ce cadre n’évoluera pas, même les acteurs les plus attachés à l’indépendance technologique continueront à avancer dans ce paradoxe : défendre la souveraineté dans le discours, tout en dépendant, dans les faits, des infrastructures des géants américains.
|
||||||
|
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
À première vue, la situation a quelque chose de déroutant. Orange, opérateur historique français, héritier d’une longue tradition de service public et acteur stratégique des télécommunications, fait massivement appel aux infrastructures cloud d’Amazon, Microsoft ou Google. Dans un contexte où la souveraineté numérique est devenue un enjeu politique, économique et géostratégique majeur, ce choix peut apparaître comme une contradiction, voire comme un renoncement. Pourtant, il ne s’agit ni d’un abandon ni d’une naïveté, mais d’un compromis révélateur des limites actuelles de l’Europe dans la bataille du numérique.
|
||||||
|
|
||||||
|
Le recours aux hyperscalers américains s’explique d’abord par une réalité industrielle. Ces entreprises ont construit, en une quinzaine d’années, une avance quasiment irréversible en matière d’infrastructures numériques. Leurs plateformes ne se contentent plus de fournir de la puissance de calcul ou du stockage. Elles offrent un écosystème complet qui va de l’hébergement à l’intelligence artificielle, en passant par l’analyse massive de données, l’automatisation des déploiements, la cybersécurité et la résilience globale des systèmes. Pour un groupe comme Orange, qui opère dans de nombreux pays et doit garantir des niveaux de service très élevés à des millions d’utilisateurs, cette maturité technologique est un facteur décisif.
|
||||||
|
|
||||||
|
Mais ce choix, rationnel sur le plan opérationnel, pose une question politique et stratégique beaucoup plus large. En confiant une part croissante de ses infrastructures numériques à des acteurs soumis au droit américain, Orange participe malgré lui à une forme de dépendance structurelle. Le Cloud Act, qui permet aux autorités américaines d’exiger l’accès à certaines données, même hébergées hors des États-Unis, symbolise cette vulnérabilité. Même si les mécanismes de chiffrement, de cloisonnement et de contrôle contractuel existent, le simple fait que la décision ultime puisse échapper aux juridictions européennes constitue une faille du point de vue de la souveraineté.
|
||||||
|
|
||||||
|
Ce paradoxe est d’autant plus frappant que des alternatives européennes existent. La France et l’Europe disposent d’acteurs solides, capables de fournir des services cloud performants, sécurisés et compétitifs sur de nombreux usages. OVHcloud, Scaleway, Outscale, IONOS ou encore les initiatives portées par Deutsche Telekom en sont la preuve. Alors pourquoi ne pas construire une alliance massive entre ces acteurs et un groupe comme Orange pour bâtir une véritable alternative souveraine ?
|
||||||
|
|
||||||
|
La réponse tient moins à la volonté qu’à l’échelle. Les hyperscalers américains investissent chaque année des dizaines de milliards d’euros dans leurs infrastructures. Ils déploient des data centers sur tous les continents, possèdent leurs propres réseaux mondiaux et attirent l’essentiel des talents du secteur. Face à cette puissance financière et industrielle, les acteurs européens, pris individuellement, peinent à rivaliser. Une alliance serait possible, mais elle nécessiterait un effort coordonné, soutenu politiquement, sur plusieurs décennies. Or l’Europe avance souvent par projets fragmentés, soumis aux cycles électoraux et aux priorités nationales parfois divergentes.
|
||||||
|
|
||||||
|
Pour Orange, la situation se résume donc à un dilemme stratégique. S’en tenir strictement à des solutions européennes aujourd’hui impliquerait souvent de renoncer à certains niveaux de performance, de standardisation et de rapidité d’innovation. Dans un marché des télécoms déjà très concurrentiel, où les marges sont sous pression et où les attentes des clients sont toujours plus élevées, ce choix pourrait fragiliser l’entreprise à court et moyen terme. À l’inverse, s’appuyer sur les clouds américains permet de rester compétitif, mais au prix d’une dépendance qui pose problème à long terme.
|
||||||
|
|
||||||
|
C’est dans cet entre-deux qu’émergent les stratégies hybrides. Orange, comme d’autres grands groupes européens, cherche à concilier deux impératifs contradictoires. D’un côté, tirer parti de la puissance des hyperscalers pour les usages nécessitant de la flexibilité, de l’innovation rapide et une échelle mondiale. De l’autre, développer des environnements de confiance pour les données sensibles, souvent en partenariat avec des acteurs européens, afin de garantir une maîtrise juridique et opérationnelle renforcée. Cette approche permet de limiter les risques sans renoncer totalement aux avantages technologiques des géants américains.
|
||||||
|
|
||||||
|
Pour celles et ceux qui défendent activement la souveraineté numérique, ce compromis reste frustrant. Il donne l’impression d’un renoncement progressif, d’une Europe qui accepte de jouer sur un terrain défini par d’autres. Pourtant, le problème dépasse largement Orange. Il interroge la capacité collective du continent à se doter d’infrastructures numériques stratégiques, au même titre que l’énergie, la défense ou les transports. Tant que le cloud restera perçu uniquement comme un marché et non comme un enjeu de souveraineté, les décisions resteront dictées par des logiques de court terme.
|
||||||
|
|
||||||
|
La vraie question n’est donc pas de savoir pourquoi Orange utilise les clouds américains, mais pourquoi l’Europe n’a pas encore su créer un cadre suffisamment ambitieux pour rendre ce choix inutile. Tant que les alternatives souveraines resteront fragmentées, sous-financées ou cantonnées à des niches, les grands groupes continueront à faire des choix pragmatiques, même s’ils sont stratégiquement inconfortables. Défendre la souveraineté numérique, ce n’est pas seulement pointer les risques de dépendance, c’est aussi accepter que cette souveraineté a un coût, qu’elle nécessite des investissements lourds, une vision industrielle de long terme et une véritable volonté politique.
|
||||||
|
|
||||||
|
Dans ce contexte, le combat pour la souveraineté numérique n’est pas perdu, mais il reste à mener sur un autre terrain que celui des seules décisions d’entreprise. Il se joue au niveau des États, de l’Union européenne et des grandes orientations industrielles. Tant que ce cadre n’évoluera pas, même les acteurs les plus attachés à l’indépendance technologique continueront à avancer dans ce paradoxe : défendre la souveraineté dans le discours, tout en dépendant, dans les faits, des infrastructures des géants américains.
|
||||||
|
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
## Orange dans les bras d'Amazon : l'aveu d'un échec européen
|
||||||
|
|
||||||
|
Orange utilise massivement AWS, Azure et Google Cloud. Dit comme ça, c'est presque une blague. L'ancien France Télécom, opérateur historique, fleuron des télécoms français, héritier du service public, branché sur les serveurs de la Silicon Valley. À l'heure où on ne parle que de souveraineté numérique, on pourrait croire à une trahison. C'est plus compliqué que ça.
|
||||||
|
|
||||||
|
La raison principale est bête : les Américains ont gagné la course. En quinze ans, AWS, Microsoft et Google ont construit une avance que personne ne sait combler aujourd'hui. Et ils ne vendent plus seulement du stockage ou de la puissance de calcul. Ils vendent un écosystème entier : de l'IA prête à l'emploi, des outils d'analyse de données, de l'automatisation, de la cybersécurité, des garanties de disponibilité à neuf chiffres. Pour Orange, qui doit faire tourner ses services dans une vingtaine de pays sans tomber en panne, ce niveau de maturité pèse lourd dans la balance.
|
||||||
|
|
||||||
|
Sauf que ce choix rationnel a un prix politique. En confiant ses infrastructures à des entreprises soumises au droit américain, Orange entre dans une zone de dépendance dont on ne sort pas facilement. Le Cloud Act permet aux autorités américaines de réclamer des données hébergées par ces sociétés, même quand ces données sont physiquement en Europe. On peut chiffrer, cloisonner, négocier des clauses dans tous les sens, le fait reste que la décision finale échappe au juge européen. Pour un opérateur télécoms qui manipule des données de millions d'abonnés, ce n'est pas un détail.
|
||||||
|
|
||||||
|
Le plus rageant, c'est qu'on a des alternatives. OVHcloud, Scaleway, Outscale, IONOS en Allemagne, sans parler des projets autour de Deutsche Telekom. Ces acteurs existent, ils sont sérieux, ils savent faire. Alors pourquoi Orange ne s'allie pas avec eux pour construire quelque chose de crédible à l'échelle européenne ?
|
||||||
|
|
||||||
|
Parce que l'écart de moyens est vertigineux. AWS et Microsoft investissent chacun plus de cinquante milliards de dollars par an dans leurs infrastructures. Ils ont leurs propres câbles sous-marins, leurs propres réseaux mondiaux, et ils raflent une bonne partie des ingénieurs qui sortent des écoles. Un OVH, même bien géré, ne joue pas dans la même catégorie financière. Il faudrait une alliance européenne soutenue politiquement, financée sur vingt ou trente ans, pour espérer rattraper. On a essayé avec Gaia-X. Le résultat parle de lui-même.
|
||||||
|
|
||||||
|
Du coup, Orange est coincé. Tout miser sur l'européen aujourd'hui, ça veut dire accepter des services moins performants, moins riches, et perdre du terrain face à ses concurrents qui, eux, n'auront pas ces scrupules. Dans un marché où les marges fondent et où chaque innovation compte, c'est un pari risqué. Continuer avec les Américains, c'est rester dans la course mais accepter une dépendance qui peut, du jour au lendemain, devenir un problème géopolitique.
|
||||||
|
|
||||||
|
D'où la solution batarde que tout le monde adopte : l'hybride. On met chez Amazon ou Microsoft ce qui doit aller vite, innover, scaler. On garde en Europe, parfois sur des clouds "de confiance" labellisés SecNumCloud, ce qui touche aux données sensibles, aux clients régulés, à l'État. Ce n'est pas glorieux, mais ça permet de tenir les deux bouts.
|
||||||
|
|
||||||
|
Pour les défenseurs de la souveraineté numérique, ce compromis a un goût amer. On a l'impression d'une Europe qui se résigne, qui joue le match sur le terrain de l'adversaire avec ses règles. Mais en pointant Orange du doigt, on rate la cible. Le vrai problème n'est pas dans les choix d'une entreprise, il est en amont. Tant qu'on traitera le cloud comme un simple marché et pas comme une infrastructure critique, au même titre que l'électricité ou les chemins de fer, les industriels feront ce qu'ils ont toujours fait : choisir ce qui marche, là, maintenant.
|
||||||
|
|
||||||
|
La bonne question n'est donc pas "pourquoi Orange utilise AWS". Elle est "pourquoi, vingt ans après l'arrivée du cloud, l'Europe n'a toujours pas mis sur la table de quoi rendre ce choix évitable". La souveraineté ne se décrète pas dans des communiqués. Elle se paie. En milliards, en années, en décisions politiques qui survivent aux changements de gouvernement. Tant qu'on ne sera pas prêts à ce niveau d'engagement, on continuera à tenir un discours sur l'indépendance numérique en signant des contrats avec Seattle et Redmond.
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
Orange utilise massivement AWS, Azure et Google Cloud. Dit comme ça, c'est presque une blague. L'ancien France Télécom, opérateur historique, fleuron des télécoms français, héritier du service public, branché sur les serveurs de la Silicon Valley. À l'heure où on ne parle que de souveraineté numérique, on pourrait croire à une trahison. C'est plus compliqué que ça.
|
||||||
|
|
||||||
|
La raison principale est bête : les Américains ont gagné la course. En quinze ans, AWS, Microsoft et Google ont construit une avance que personne ne sait combler aujourd'hui. Et ils ne vendent plus seulement du stockage ou de la puissance de calcul. Ils vendent un écosystème entier : de l'IA prête à l'emploi, des outils d'analyse de données, de l'automatisation, de la cybersécurité, des garanties de disponibilité à neuf chiffres. Pour Orange, qui doit faire tourner ses services dans une vingtaine de pays sans tomber en panne, ce niveau de maturité pèse lourd dans la balance.
|
||||||
|
|
||||||
|
Sauf que ce choix rationnel a un prix politique. En confiant ses infrastructures à des entreprises soumises au droit américain, Orange entre dans une zone de dépendance dont on ne sort pas facilement. Le Cloud Act permet aux autorités américaines de réclamer des données hébergées par ces sociétés, même quand ces données sont physiquement en Europe. On peut chiffrer, cloisonner, négocier des clauses dans tous les sens, le fait reste que la décision finale échappe au juge européen. Pour un opérateur télécoms qui manipule des données de millions d'abonnés, ce n'est pas un détail.
|
||||||
|
|
||||||
|
Le plus rageant, c'est qu'on a des alternatives. OVHcloud, Scaleway, Outscale, IONOS en Allemagne, sans parler des projets autour de Deutsche Telekom. Ces acteurs existent, ils sont sérieux, ils savent faire. Alors pourquoi Orange ne s'allie pas avec eux pour construire quelque chose de crédible à l'échelle européenne ?
|
||||||
|
|
||||||
|
Parce que l'écart de moyens est vertigineux. AWS et Microsoft investissent chacun plus de cinquante milliards de dollars par an dans leurs infrastructures. Ils ont leurs propres câbles sous-marins, leurs propres réseaux mondiaux, et ils raflent une bonne partie des ingénieurs qui sortent des écoles. Un OVH, même bien géré, ne joue pas dans la même catégorie financière. Il faudrait une alliance européenne soutenue politiquement, financée sur vingt ou trente ans, pour espérer rattraper. On a essayé avec Gaia-X. Le résultat parle de lui-même.
|
||||||
|
|
||||||
|
Du coup, Orange est coincé. Tout miser sur l'européen aujourd'hui, ça veut dire accepter des services moins performants, moins riches, et perdre du terrain face à ses concurrents qui, eux, n'auront pas ces scrupules. Dans un marché où les marges fondent et où chaque innovation compte, c'est un pari risqué. Continuer avec les Américains, c'est rester dans la course mais accepter une dépendance qui peut, du jour au lendemain, devenir un problème géopolitique.
|
||||||
|
|
||||||
|
D'où la solution batarde que tout le monde adopte : l'hybride. On met chez Amazon ou Microsoft ce qui doit aller vite, innover, scaler. On garde en Europe, parfois sur des clouds "de confiance" labellisés SecNumCloud, ce qui touche aux données sensibles, aux clients régulés, à l'État. Ce n'est pas glorieux, mais ça permet de tenir les deux bouts.
|
||||||
|
|
||||||
|
Pour les défenseurs de la souveraineté numérique, ce compromis a un goût amer. On a l'impression d'une Europe qui se résigne, qui joue le match sur le terrain de l'adversaire avec ses règles. Mais en pointant Orange du doigt, on rate la cible. Le vrai problème n'est pas dans les choix d'une entreprise, il est en amont. Tant qu'on traitera le cloud comme un simple marché et pas comme une infrastructure critique, au même titre que l'électricité ou les chemins de fer, les industriels feront ce qu'ils ont toujours fait : choisir ce qui marche, là, maintenant.
|
||||||
|
|
||||||
|
La bonne question n'est donc pas "pourquoi Orange utilise AWS". Elle est "pourquoi, vingt ans après l'arrivée du cloud, l'Europe n'a toujours pas mis sur la table de quoi rendre ce choix évitable". La souveraineté ne se décrète pas dans des communiqués. Elle se paie. En milliards, en années, en décisions politiques qui survivent aux changements de gouvernement. Tant qu'on ne sera pas prêts à ce niveau d'engagement, on continuera à tenir un discours sur l'indépendance numérique en signant des contrats avec Seattle et Redmond.
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
Orange utilise massivement AWS, Azure et Google Cloud. Dit comme ça, c'est presque une blague. L'ancien France Télécom, opérateur historique, fleuron des télécoms français, héritier du service public, branché sur les serveurs de la Silicon Valley. À l'heure où on ne parle que de souveraineté numérique, on pourrait croire à une trahison. C'est plus compliqué que ça.
|
||||||
|
|
||||||
|
La raison principale est bête : les Américains ont gagné la course. En quinze ans, AWS, Microsoft et Google ont construit une avance que personne ne sait combler aujourd'hui. Et ils ne vendent plus seulement du stockage ou de la puissance de calcul. Ils vendent un écosystème entier : de l'IA prête à l'emploi, des outils d'analyse de données, de l'automatisation, de la cybersécurité, des garanties de disponibilité à neuf chiffres. Pour Orange, qui doit faire tourner ses services dans une vingtaine de pays sans tomber en panne, ce niveau de maturité pèse lourd dans la balance.
|
||||||
|
|
||||||
|
Sauf que ce choix rationnel a un prix politique. En confiant ses infrastructures à des entreprises soumises au droit américain, Orange entre dans une zone de dépendance dont on ne sort pas facilement. Le Cloud Act permet aux autorités américaines de réclamer des données hébergées par ces sociétés, même quand ces données sont physiquement en Europe. On peut chiffrer, cloisonner, négocier des clauses dans tous les sens, le fait reste que la décision finale échappe au juge européen. Pour un opérateur télécoms qui manipule des données de millions d'abonnés, ce n'est pas un détail.
|
||||||
|
|
||||||
|
Le plus rageant, c'est qu'on a des alternatives. OVHcloud, Scaleway, Outscale, IONOS en Allemagne, sans parler des projets autour de Deutsche Telekom. Ces acteurs existent, ils sont sérieux, ils savent faire. Alors pourquoi Orange ne s'allie pas avec eux pour construire quelque chose de crédible à l'échelle européenne ?
|
||||||
|
|
||||||
|
Parce que l'écart de moyens est vertigineux. AWS et Microsoft investissent chacun plus de cinquante milliards de dollars par an dans leurs infrastructures. Ils ont leurs propres câbles sous-marins, leurs propres réseaux mondiaux, et ils raflent une bonne partie des ingénieurs qui sortent des écoles. Un OVH, même bien géré, ne joue pas dans la même catégorie financière. Il faudrait une alliance européenne soutenue politiquement, financée sur vingt ou trente ans, pour espérer rattraper. On a essayé avec Gaia-X. Le résultat parle de lui-même.
|
||||||
|
|
||||||
|
Du coup, Orange est coincé. Tout miser sur l'européen aujourd'hui, ça veut dire accepter des services moins performants, moins riches, et perdre du terrain face à ses concurrents qui, eux, n'auront pas ces scrupules. Dans un marché où les marges fondent et où chaque innovation compte, c'est un pari risqué. Continuer avec les Américains, c'est rester dans la course mais accepter une dépendance qui peut, du jour au lendemain, devenir un problème géopolitique.
|
||||||
|
|
||||||
|
D'où la solution batarde que tout le monde adopte : l'hybride. On met chez Amazon ou Microsoft ce qui doit aller vite, innover, scaler. On garde en Europe, parfois sur des clouds "de confiance" labellisés SecNumCloud, ce qui touche aux données sensibles, aux clients régulés, à l'État. Ce n'est pas glorieux, mais ça permet de tenir les deux bouts.
|
||||||
|
|
||||||
|
Pour les défenseurs de la souveraineté numérique, ce compromis a un goût amer. On a l'impression d'une Europe qui se résigne, qui joue le match sur le terrain de l'adversaire avec ses règles. Mais en pointant Orange du doigt, on rate la cible. Le vrai problème n'est pas dans les choix d'une entreprise, il est en amont. Tant qu'on traitera le cloud comme un simple marché et pas comme une infrastructure critique, au même titre que l'électricité ou les chemins de fer, les industriels feront ce qu'ils ont toujours fait : choisir ce qui marche, là, maintenant.
|
||||||
|
|
||||||
|
La bonne question n'est donc pas "pourquoi Orange utilise AWS". Elle est "pourquoi, vingt ans après l'arrivée du cloud, l'Europe n'a toujours pas mis sur la table de quoi rendre ce choix évitable". La souveraineté ne se décrète pas dans des communiqués. Elle se paie. En milliards, en années, en décisions politiques qui survivent aux changements de gouvernement. Tant qu'on ne sera pas prêts à ce niveau d'engagement, on continuera à tenir un discours sur l'indépendance numérique en signant des contrats avec Seattle et Redmond.
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
Orange utilise massivement AWS, Azure et Google Cloud. Dit comme ça, c'est presque une blague. L'ancien France Télécom, opérateur historique, fleuron des télécoms français, héritier du service public, branché sur les serveurs de la Silicon Valley. À l'heure où on ne parle que de souveraineté numérique, on pourrait croire à une trahison. C'est plus compliqué que ça.
|
||||||
|
|
||||||
|
La raison principale est bête : les Américains ont gagné la course. En quinze ans, AWS, Microsoft et Google ont construit une avance que personne ne sait combler aujourd'hui. Et ils ne vendent plus seulement du stockage ou de la puissance de calcul. Ils vendent un écosystème entier : de l'IA prête à l'emploi, des outils d'analyse de données, de l'automatisation, de la cybersécurité, des garanties de disponibilité à neuf chiffres. Pour Orange, qui doit faire tourner ses services dans une vingtaine de pays sans tomber en panne, ce niveau de maturité pèse lourd dans la balance.
|
||||||
|
|
||||||
|
Sauf que ce choix rationnel a un prix politique. En confiant ses infrastructures à des entreprises soumises au droit américain, Orange entre dans une zone de dépendance dont on ne sort pas facilement. Le Cloud Act permet aux autorités américaines de réclamer des données hébergées par ces sociétés, même quand ces données sont physiquement en Europe. On peut chiffrer, cloisonner, négocier des clauses dans tous les sens, le fait reste que la décision finale échappe au juge européen. Pour un opérateur télécoms qui manipule des données de millions d'abonnés, ce n'est pas un détail.
|
||||||
|
|
||||||
|
Le plus rageant, c'est qu'on a des alternatives. OVHcloud, Scaleway, Outscale, IONOS en Allemagne, sans parler des projets autour de Deutsche Telekom. Ces acteurs existent, ils sont sérieux, ils savent faire. Alors pourquoi Orange ne s'allie pas avec eux pour construire quelque chose de crédible à l'échelle européenne ?
|
||||||
|
|
||||||
|
Parce que l'écart de moyens est vertigineux. AWS et Microsoft investissent chacun plus de cinquante milliards de dollars par an dans leurs infrastructures. Ils ont leurs propres câbles sous-marins, leurs propres réseaux mondiaux, et ils raflent une bonne partie des ingénieurs qui sortent des écoles. Un OVH, même bien géré, ne joue pas dans la même catégorie financière. Il faudrait une alliance européenne soutenue politiquement, financée sur vingt ou trente ans, pour espérer rattraper. On a essayé avec Gaia-X. Le résultat parle de lui-même.
|
||||||
|
|
||||||
|
Du coup, Orange est coincé. Tout miser sur l'européen aujourd'hui, ça veut dire accepter des services moins performants, moins riches, et perdre du terrain face à ses concurrents qui, eux, n'auront pas ces scrupules. Dans un marché où les marges fondent et où chaque innovation compte, c'est un pari risqué. Continuer avec les Américains, c'est rester dans la course mais accepter une dépendance qui peut, du jour au lendemain, devenir un problème géopolitique.
|
||||||
|
|
||||||
|
D'où la solution batarde que tout le monde adopte : l'hybride. On met chez Amazon ou Microsoft ce qui doit aller vite, innover, scaler. On garde en Europe, parfois sur des clouds "de confiance" labellisés SecNumCloud, ce qui touche aux données sensibles, aux clients régulés, à l'État. Ce n'est pas glorieux, mais ça permet de tenir les deux bouts.
|
||||||
|
|
||||||
|
Pour les défenseurs de la souveraineté numérique, ce compromis a un goût amer. On a l'impression d'une Europe qui se résigne, qui joue le match sur le terrain de l'adversaire avec ses règles. Mais en pointant Orange du doigt, on rate la cible. Le vrai problème n'est pas dans les choix d'une entreprise, il est en amont. Tant qu'on traitera le cloud comme un simple marché et pas comme une infrastructure critique, au même titre que l'électricité ou les chemins de fer, les industriels feront ce qu'ils ont toujours fait : choisir ce qui marche, là, maintenant.
|
||||||
|
|
||||||
|
La bonne question n'est donc pas "pourquoi Orange utilise AWS". Elle est "pourquoi, vingt ans après l'arrivée du cloud, l'Europe n'a toujours pas mis sur la table de quoi rendre ce choix évitable". La souveraineté ne se décrète pas dans des communiqués. Elle se paie. En milliards, en années, en décisions politiques qui survivent aux changements de gouvernement. Tant qu'on ne sera pas prêts à ce niveau d'engagement, on continuera à tenir un discours sur l'indépendance numérique en signant des contrats avec Seattle et Redmond.
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
Orange utilise massivement AWS, Azure et Google Cloud. Dit comme ça, c'est presque une blague. L'ancien France Télécom, opérateur historique, fleuron des télécoms français, héritier du service public, branché sur les serveurs de la Silicon Valley. À l'heure où on ne parle que de souveraineté numérique, on pourrait croire à une trahison. C'est plus compliqué que ça.
|
||||||
|
|
||||||
|
La raison principale est bête : les Américains ont gagné la course. En quinze ans, AWS, Microsoft et Google ont construit une avance que personne ne sait combler aujourd'hui. Et ils ne vendent plus seulement du stockage ou de la puissance de calcul. Ils vendent un écosystème entier : de l'IA prête à l'emploi, des outils d'analyse de données, de l'automatisation, de la cybersécurité, des garanties de disponibilité à neuf chiffres. Pour Orange, qui doit faire tourner ses services dans une vingtaine de pays sans tomber en panne, ce niveau de maturité pèse lourd dans la balance.
|
||||||
|
|
||||||
|
Sauf que ce choix rationnel a un prix politique. En confiant ses infrastructures à des entreprises soumises au droit américain, Orange entre dans une zone de dépendance dont on ne sort pas facilement. Le Cloud Act permet aux autorités américaines de réclamer des données hébergées par ces sociétés, même quand ces données sont physiquement en Europe. On peut chiffrer, cloisonner, négocier des clauses dans tous les sens, le fait reste que la décision finale échappe au juge européen. Pour un opérateur télécoms qui manipule des données de millions d'abonnés, ce n'est pas un détail.
|
||||||
|
|
||||||
|
Le plus rageant, c'est qu'on a des alternatives. OVHcloud, Scaleway, Outscale, IONOS en Allemagne, sans parler des projets autour de Deutsche Telekom. Ces acteurs existent, ils sont sérieux, ils savent faire. Alors pourquoi Orange ne s'allie pas avec eux pour construire quelque chose de crédible à l'échelle européenne ?
|
||||||
|
|
||||||
|
Parce que l'écart de moyens est vertigineux. AWS et Microsoft investissent chacun plus de cinquante milliards de dollars par an dans leurs infrastructures. Ils ont leurs propres câbles sous-marins, leurs propres réseaux mondiaux, et ils raflent une bonne partie des ingénieurs qui sortent des écoles. Un OVH, même bien géré, ne joue pas dans la même catégorie financière. Il faudrait une alliance européenne soutenue politiquement, financée sur vingt ou trente ans, pour espérer rattraper. On a essayé avec Gaia-X. Le résultat parle de lui-même.
|
||||||
|
|
||||||
|
Du coup, Orange est coincé. Tout miser sur l'européen aujourd'hui, ça veut dire accepter des services moins performants, moins riches, et perdre du terrain face à ses concurrents qui, eux, n'auront pas ces scrupules. Dans un marché où les marges fondent et où chaque innovation compte, c'est un pari risqué. Continuer avec les Américains, c'est rester dans la course mais accepter une dépendance qui peut, du jour au lendemain, devenir un problème géopolitique.
|
||||||
|
|
||||||
|
D'où la solution batarde que tout le monde adopte : l'hybride. On met chez Amazon ou Microsoft ce qui doit aller vite, innover, scaler. On garde en Europe, parfois sur des clouds "de confiance" labellisés SecNumCloud, ce qui touche aux données sensibles, aux clients régulés, à l'État. Ce n'est pas glorieux, mais ça permet de tenir les deux bouts.
|
||||||
|
|
||||||
|
Pour les défenseurs de la souveraineté numérique, ce compromis a un goût amer. On a l'impression d'une Europe qui se résigne, qui joue le match sur le terrain de l'adversaire avec ses règles. Mais en pointant Orange du doigt, on rate la cible. Le vrai problème n'est pas dans les choix d'une entreprise, il est en amont. Tant qu'on traitera le cloud comme un simple marché et pas comme une infrastructure critique, au même titre que l'électricité ou les chemins de fer, les industriels feront ce qu'ils ont toujours fait : choisir ce qui marche, là, maintenant.
|
||||||
|
|
||||||
|
La bonne question n'est donc pas "pourquoi Orange utilise AWS". Elle est "pourquoi, vingt ans après l'arrivée du cloud, l'Europe n'a toujours pas mis sur la table de quoi rendre ce choix évitable". La souveraineté ne se décrète pas dans des communiqués. Elle se paie. En milliards, en années, en décisions politiques qui survivent aux changements de gouvernement. Tant qu'on ne sera pas prêts à ce niveau d'engagement, on continuera à tenir un discours sur l'indépendance numérique en signant des contrats avec Seattle et Redmond.
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
Orange utilise massivement AWS, Azure et Google Cloud. Dit comme ça, c'est presque une blague. L'ancien France Télécom, opérateur historique, fleuron des télécoms français, héritier du service public, branché sur les serveurs de la Silicon Valley. À l'heure où on ne parle que de souveraineté numérique, on pourrait croire à une trahison. C'est plus compliqué que ça.
|
||||||
|
|
||||||
|
La raison principale est bête : les Américains ont gagné la course. En quinze ans, AWS, Microsoft et Google ont construit une avance que personne ne sait combler aujourd'hui. Et ils ne vendent plus seulement du stockage ou de la puissance de calcul. Ils vendent un écosystème entier : de l'IA prête à l'emploi, des outils d'analyse de données, de l'automatisation, de la cybersécurité, des garanties de disponibilité à neuf chiffres. Pour Orange, qui doit faire tourner ses services dans une vingtaine de pays sans tomber en panne, ce niveau de maturité pèse lourd dans la balance.
|
||||||
|
|
||||||
|
Sauf que ce choix rationnel a un prix politique. En confiant ses infrastructures à des entreprises soumises au droit américain, Orange entre dans une zone de dépendance dont on ne sort pas facilement. Le Cloud Act permet aux autorités américaines de réclamer des données hébergées par ces sociétés, même quand ces données sont physiquement en Europe. On peut chiffrer, cloisonner, négocier des clauses dans tous les sens, le fait reste que la décision finale échappe au juge européen. Pour un opérateur télécoms qui manipule des données de millions d'abonnés, ce n'est pas un détail.
|
||||||
|
|
||||||
|
Le plus rageant, c'est qu'on a des alternatives. OVHcloud, Scaleway, Outscale, IONOS en Allemagne, sans parler des projets autour de Deutsche Telekom. Ces acteurs existent, ils sont sérieux, ils savent faire. Alors pourquoi Orange ne s'allie pas avec eux pour construire quelque chose de crédible à l'échelle européenne ?
|
||||||
|
|
||||||
|
Parce que l'écart de moyens est vertigineux. AWS et Microsoft investissent chacun plus de cinquante milliards de dollars par an dans leurs infrastructures. Ils ont leurs propres câbles sous-marins, leurs propres réseaux mondiaux, et ils raflent une bonne partie des ingénieurs qui sortent des écoles. Un OVH, même bien géré, ne joue pas dans la même catégorie financière. Il faudrait une alliance européenne soutenue politiquement, financée sur vingt ou trente ans, pour espérer rattraper. On a essayé avec Gaia-X. Le résultat parle de lui-même.
|
||||||
|
|
||||||
|
Du coup, Orange est coincé. Tout miser sur l'européen aujourd'hui, ça veut dire accepter des services moins performants, moins riches, et perdre du terrain face à ses concurrents qui, eux, n'auront pas ces scrupules. Dans un marché où les marges fondent et où chaque innovation compte, c'est un pari risqué. Continuer avec les Américains, c'est rester dans la course mais accepter une dépendance qui peut, du jour au lendemain, devenir un problème géopolitique.
|
||||||
|
|
||||||
|
D'où la solution batarde que tout le monde adopte : l'hybride. On met chez Amazon ou Microsoft ce qui doit aller vite, innover, scaler. On garde en Europe, parfois sur des clouds "de confiance" labellisés SecNumCloud, ce qui touche aux données sensibles, aux clients régulés, à l'État. Ce n'est pas glorieux, mais ça permet de tenir les deux bouts.
|
||||||
|
|
||||||
|
Pour les défenseurs de la souveraineté numérique, ce compromis a un goût amer. On a l'impression d'une Europe qui se résigne, qui joue le match sur le terrain de l'adversaire avec ses règles. Mais en pointant Orange du doigt, on rate la cible. Le vrai problème n'est pas dans les choix d'une entreprise, il est en amont. Tant qu'on traitera le cloud comme un simple marché et pas comme une infrastructure critique, au même titre que l'électricité ou les chemins de fer, les industriels feront ce qu'ils ont toujours fait : choisir ce qui marche, là, maintenant.
|
||||||
|
|
||||||
|
La bonne question n'est donc pas "pourquoi Orange utilise AWS". Elle est "pourquoi, vingt ans après l'arrivée du cloud, l'Europe n'a toujours pas mis sur la table de quoi rendre ce choix évitable". La souveraineté ne se décrète pas dans des communiqués. Elle se paie. En milliards, en années, en décisions politiques qui survivent aux changements de gouvernement. Tant qu'on ne sera pas prêts à ce niveau d'engagement, on continuera à tenir un discours sur l'indépendance numérique en signant des contrats avec Seattle et Redmond.
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
Orange utilise massivement AWS, Azure et Google Cloud. Dit comme ça, c'est presque une blague. L'ancien France Télécom, opérateur historique, fleuron des télécoms français, héritier du service public, branché sur les serveurs de la Silicon Valley. À l'heure où on ne parle que de souveraineté numérique, on pourrait croire à une trahison. C'est plus compliqué que ça.
|
||||||
|
|
||||||
|
La raison principale est bête : les Américains ont gagné la course. En quinze ans, AWS, Microsoft et Google ont construit une avance que personne ne sait combler aujourd'hui. Et ils ne vendent plus seulement du stockage ou de la puissance de calcul. Ils vendent un écosystème entier : de l'IA prête à l'emploi, des outils d'analyse de données, de l'automatisation, de la cybersécurité, des garanties de disponibilité à neuf chiffres. Pour Orange, qui doit faire tourner ses services dans une vingtaine de pays sans tomber en panne, ce niveau de maturité pèse lourd dans la balance.
|
||||||
|
|
||||||
|
Sauf que ce choix rationnel a un prix politique. En confiant ses infrastructures à des entreprises soumises au droit américain, Orange entre dans une zone de dépendance dont on ne sort pas facilement. Le Cloud Act permet aux autorités américaines de réclamer des données hébergées par ces sociétés, même quand ces données sont physiquement en Europe. On peut chiffrer, cloisonner, négocier des clauses dans tous les sens, le fait reste que la décision finale échappe au juge européen. Pour un opérateur télécoms qui manipule des données de millions d'abonnés, ce n'est pas un détail.
|
||||||
|
|
||||||
|
Le plus rageant, c'est qu'on a des alternatives. OVHcloud, Scaleway, Outscale, IONOS en Allemagne, sans parler des projets autour de Deutsche Telekom. Ces acteurs existent, ils sont sérieux, ils savent faire. Alors pourquoi Orange ne s'allie pas avec eux pour construire quelque chose de crédible à l'échelle européenne ?
|
||||||
|
|
||||||
|
Parce que l'écart de moyens est vertigineux. AWS et Microsoft investissent chacun plus de cinquante milliards de dollars par an dans leurs infrastructures. Ils ont leurs propres câbles sous-marins, leurs propres réseaux mondiaux, et ils raflent une bonne partie des ingénieurs qui sortent des écoles. Un OVH, même bien géré, ne joue pas dans la même catégorie financière. Il faudrait une alliance européenne soutenue politiquement, financée sur vingt ou trente ans, pour espérer rattraper. On a essayé avec Gaia-X. Le résultat parle de lui-même.
|
||||||
|
|
||||||
|
Du coup, Orange est coincé. Tout miser sur l'européen aujourd'hui, ça veut dire accepter des services moins performants, moins riches, et perdre du terrain face à ses concurrents qui, eux, n'auront pas ces scrupules. Dans un marché où les marges fondent et où chaque innovation compte, c'est un pari risqué. Continuer avec les Américains, c'est rester dans la course mais accepter une dépendance qui peut, du jour au lendemain, devenir un problème géopolitique.
|
||||||
|
|
||||||
|
D'où la solution batarde que tout le monde adopte : l'hybride. On met chez Amazon ou Microsoft ce qui doit aller vite, innover, scaler. On garde en Europe, parfois sur des clouds "de confiance" labellisés SecNumCloud, ce qui touche aux données sensibles, aux clients régulés, à l'État. Ce n'est pas glorieux, mais ça permet de tenir les deux bouts.
|
||||||
|
|
||||||
|
Pour les défenseurs de la souveraineté numérique, ce compromis a un goût amer. On a l'impression d'une Europe qui se résigne, qui joue le match sur le terrain de l'adversaire avec ses règles. Mais en pointant Orange du doigt, on rate la cible. Le vrai problème n'est pas dans les choix d'une entreprise, il est en amont. Tant qu'on traitera le cloud comme un simple marché et pas comme une infrastructure critique, au même titre que l'électricité ou les chemins de fer, les industriels feront ce qu'ils ont toujours fait : choisir ce qui marche, là, maintenant.
|
||||||
|
|
||||||
|
La bonne question n'est donc pas "pourquoi Orange utilise AWS". Elle est "pourquoi, vingt ans après l'arrivée du cloud, l'Europe n'a toujours pas mis sur la table de quoi rendre ce choix évitable". La souveraineté ne se décrète pas dans des communiqués. Elle se paie. En milliards, en années, en décisions politiques qui survivent aux changements de gouvernement. Tant qu'on ne sera pas prêts à ce niveau d'engagement, on continuera à tenir un discours sur l'indépendance numérique en signant des contrats avec Seattle et Redmond.
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
<svg width="100%" viewBox="0 0 690 470" role="img" xmlns="http://www.w3.org/2000/svg" style="">
|
||||||
|
<title style="fill:rgb(0, 0, 0);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">Mises à jour automatiques de sécurité sur Debian</title>
|
||||||
|
<desc style="fill:rgb(0, 0, 0);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">Illustration symbolique : un bouclier protégeant un terminal Debian, entouré de paquets de mise à jour en orbite et de flux de données sécurisés.</desc>
|
||||||
|
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="bgGrad" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="0" stop-color="#0f1626"/>
|
||||||
|
<stop offset="1" stop-color="#1a2942"/>
|
||||||
|
</linearGradient>
|
||||||
|
<clipPath id="screenClip">
|
||||||
|
<rect x="240" y="200" width="200" height="120" rx="4"/>
|
||||||
|
</clipPath>
|
||||||
|
<mask id="imagine-text-gaps-g4glik" maskUnits="userSpaceOnUse"><rect x="0" y="0" width="690" height="470" fill="white"/><rect x="-14.833333015441895" y="-7.75" width="29.66666603088379" height="15.25" fill="black" rx="2"/><rect x="-14.833333015441895" y="-7.75" width="29.66666603088379" height="15.25" fill="black" rx="2"/><rect x="-22.95833396911621" y="-7.75" width="45.91666793823242" height="15.25" fill="black" rx="2"/><rect x="-17.541666984558105" y="-7.75" width="35.08333396911621" height="15.25" fill="black" rx="2"/><rect x="-12.125" y="-7.75" width="24.25" height="15.25" fill="black" rx="2"/><rect x="273.5666809082031" y="162.5" width="142.86666870117188" height="14" fill="black" rx="2"/><rect x="264" y="176.75" width="80.78333282470703" height="19" fill="black" rx="2"/><rect x="334" y="176.75" width="74.16666412353516" height="19" fill="black" rx="2"/><rect x="264" y="190.75" width="180.03334045410156" height="19" fill="black" rx="2"/><rect x="264" y="204.75" width="80.78333282470703" height="19" fill="black" rx="2"/><rect x="334" y="204.75" width="127.0999984741211" height="19" fill="black" rx="2"/><rect x="264" y="218.75" width="186.64999389648438" height="19" fill="black" rx="2"/><rect x="264" y="232.75" width="113.86666870117188" height="19" fill="black" rx="2"/><rect x="264" y="246.75" width="146.9499969482422" height="19" fill="black" rx="2"/><rect x="264" y="260.75" width="120.48332977294922" height="19" fill="black" rx="2"/><rect x="264" y="276.75" width="160.18333435058594" height="19" fill="black" rx="2"/><rect x="264" y="294.75" width="80.78333282470703" height="19" fill="black" rx="2"/><rect x="334" y="294.75" width="14.616666793823242" height="19" fill="black" rx="2"/><rect x="156.05833435058594" y="18" width="372.0666809082031" height="29" fill="black" rx="2"/><rect x="243.2083282470703" y="47.5" width="196.3333282470703" height="20.25" fill="black" rx="2"/><rect x="206.97500610351562" y="406.75" width="272.6666564941406" height="19" fill="black" rx="2"/><rect x="219.56666564941406" y="426.75" width="244.06666564941406" height="20.25" fill="black" rx="2"/></mask></defs>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Background -->
|
||||||
|
<rect x="0" y="0" width="680" height="460" style="stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||||
|
|
||||||
|
<!-- Decorative grid -->
|
||||||
|
<g opacity="0.18" style="fill:rgb(0, 0, 0);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:0.18;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
|
||||||
|
<line x1="0" y1="100" x2="680" y2="100" stroke-width="0.5" style="fill:rgb(0, 0, 0);stroke:rgb(42, 58, 90);color:rgb(0, 0, 0);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||||
|
<line x1="0" y1="200" x2="680" y2="200" stroke-width="0.5" mask="url(#imagine-text-gaps-g4glik)" style="fill:rgb(0, 0, 0);stroke:rgb(42, 58, 90);color:rgb(0, 0, 0);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||||
|
<line x1="0" y1="300" x2="680" y2="300" stroke-width="0.5" mask="url(#imagine-text-gaps-g4glik)" style="fill:rgb(0, 0, 0);stroke:rgb(42, 58, 90);color:rgb(0, 0, 0);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||||
|
<line x1="0" y1="400" x2="680" y2="400" stroke-width="0.5" style="fill:rgb(0, 0, 0);stroke:rgb(42, 58, 90);color:rgb(0, 0, 0);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||||
|
<line x1="170" y1="0" x2="170" y2="460" stroke-width="0.5" mask="url(#imagine-text-gaps-g4glik)" style="fill:rgb(0, 0, 0);stroke:rgb(42, 58, 90);color:rgb(0, 0, 0);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||||
|
<line x1="340" y1="0" x2="340" y2="460" stroke-width="0.5" mask="url(#imagine-text-gaps-g4glik)" style="fill:rgb(0, 0, 0);stroke:rgb(42, 58, 90);color:rgb(0, 0, 0);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||||
|
<line x1="510" y1="0" x2="510" y2="460" stroke-width="0.5" mask="url(#imagine-text-gaps-g4glik)" style="fill:rgb(0, 0, 0);stroke:rgb(42, 58, 90);color:rgb(0, 0, 0);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Decorative stars / dots -->
|
||||||
|
<circle cx="80" cy="60" r="1.5" style="fill:rgb(107, 122, 153);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||||
|
<circle cx="600" cy="80" r="2" style="fill:rgb(107, 122, 153);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||||
|
<circle cx="120" cy="380" r="1.5" style="fill:rgb(107, 122, 153);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||||
|
<circle cx="580" cy="400" r="1.5" style="fill:rgb(107, 122, 153);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||||
|
<circle cx="50" cy="220" r="1" style="fill:rgb(107, 122, 153);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||||
|
<circle cx="630" cy="250" r="1.5" style="fill:rgb(107, 122, 153);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||||
|
<circle cx="640" cy="160" r="1" style="fill:rgb(107, 122, 153);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||||
|
<circle cx="40" cy="320" r="1" style="fill:rgb(107, 122, 153);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||||
|
|
||||||
|
<!-- Orbital rings around the shield -->
|
||||||
|
<g fill="none" opacity="0.35" style="fill:none;stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:0.35;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
|
||||||
|
<ellipse cx="340" cy="240" rx="260" ry="80" stroke="#3a5a8a" stroke-width="0.5" stroke-dasharray="2 4" style="fill:none;stroke:rgb(58, 90, 138);color:rgb(0, 0, 0);stroke-width:0.5px;stroke-dasharray:2px, 4px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||||
|
<ellipse cx="340" cy="240" rx="220" ry="60" stroke="#4a7aaa" stroke-width="0.5" stroke-dasharray="3 3" style="fill:none;stroke:rgb(74, 122, 170);color:rgb(0, 0, 0);stroke-width:0.5px;stroke-dasharray:3px, 3px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Orbiting update packets -->
|
||||||
|
<g style="fill:rgb(0, 0, 0);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
|
||||||
|
<!-- Packet left -->
|
||||||
|
<g transform="translate(85, 235)" style="fill:rgb(0, 0, 0);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
|
||||||
|
<rect x="-18" y="-12" width="36" height="24" rx="3" fill="#1e3a5f" stroke="#d70751" stroke-width="1" style="fill:rgb(30, 58, 95);stroke:rgb(215, 7, 81);color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||||
|
<text x="0" y="3" text-anchor="middle" font-family="ui-monospace, monospace" font-size="9" fill="#ffb8c8" style="fill:rgb(255, 184, 200);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:ui-monospace, monospace;font-size:9px;font-weight:400;text-anchor:middle;dominant-baseline:auto">.deb</text>
|
||||||
|
</g>
|
||||||
|
<!-- Packet right -->
|
||||||
|
<g transform="translate(595, 245)" style="fill:rgb(0, 0, 0);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
|
||||||
|
<rect x="-18" y="-12" width="36" height="24" rx="3" fill="#1e3a5f" stroke="#d70751" stroke-width="1" style="fill:rgb(30, 58, 95);stroke:rgb(215, 7, 81);color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||||
|
<text x="0" y="3" text-anchor="middle" font-family="ui-monospace, monospace" font-size="9" fill="#ffb8c8" style="fill:rgb(255, 184, 200);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:ui-monospace, monospace;font-size:9px;font-weight:400;text-anchor:middle;dominant-baseline:auto">.deb</text>
|
||||||
|
</g>
|
||||||
|
<!-- Packet top -->
|
||||||
|
<g transform="translate(340, 165)" style="fill:rgb(0, 0, 0);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
|
||||||
|
<rect x="-22" y="-12" width="44" height="24" rx="3" fill="#1e3a5f" stroke="#7fffaf" stroke-width="1" style="fill:rgb(30, 58, 95);stroke:rgb(127, 255, 175);color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||||
|
<text x="0" y="3" text-anchor="middle" font-family="ui-monospace, monospace" font-size="9" fill="#7fffaf" style="fill:rgb(127, 255, 175);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:ui-monospace, monospace;font-size:9px;font-weight:400;text-anchor:middle;dominant-baseline:auto">CVE-fix</text>
|
||||||
|
</g>
|
||||||
|
<!-- Packet bottom-left -->
|
||||||
|
<g transform="translate(170, 310)" style="fill:rgb(0, 0, 0);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
|
||||||
|
<rect x="-20" y="-12" width="40" height="24" rx="3" fill="#1e3a5f" stroke="#ffd166" stroke-width="1" style="fill:rgb(30, 58, 95);stroke:rgb(255, 209, 102);color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||||
|
<text x="0" y="3" text-anchor="middle" font-family="ui-monospace, monospace" font-size="9" fill="#ffd166" style="fill:rgb(255, 209, 102);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:ui-monospace, monospace;font-size:9px;font-weight:400;text-anchor:middle;dominant-baseline:auto">patch</text>
|
||||||
|
</g>
|
||||||
|
<!-- Packet bottom-right -->
|
||||||
|
<g transform="translate(510, 305)" style="fill:rgb(0, 0, 0);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
|
||||||
|
<rect x="-18" y="-12" width="36" height="24" rx="3" fill="#1e3a5f" stroke="#7fffaf" stroke-width="1" style="fill:rgb(30, 58, 95);stroke:rgb(127, 255, 175);color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||||
|
<text x="0" y="3" text-anchor="middle" font-family="ui-monospace, monospace" font-size="9" fill="#7fffaf" style="fill:rgb(127, 255, 175);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:ui-monospace, monospace;font-size:9px;font-weight:400;text-anchor:middle;dominant-baseline:auto">apt</text>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Arrows showing packets flowing toward the shield -->
|
||||||
|
<g stroke="#5a8aba" stroke-width="0.8" fill="none" opacity="0.6" stroke-dasharray="2 3" style="fill:none;stroke:rgb(90, 138, 186);color:rgb(0, 0, 0);stroke-width:0.8px;stroke-dasharray:2px, 3px;stroke-linecap:butt;stroke-linejoin:miter;opacity:0.6;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
|
||||||
|
<path d="M 110 235 Q 200 230 240 235" style="fill:none;stroke:rgb(90, 138, 186);color:rgb(0, 0, 0);stroke-width:0.8px;stroke-dasharray:2px, 3px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||||
|
<path d="M 570 245 Q 480 240 440 240" style="fill:none;stroke:rgb(90, 138, 186);color:rgb(0, 0, 0);stroke-width:0.8px;stroke-dasharray:2px, 3px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||||
|
<path d="M 340 180 L 340 200" mask="url(#imagine-text-gaps-g4glik)" style="fill:none;stroke:rgb(90, 138, 186);color:rgb(0, 0, 0);stroke-width:0.8px;stroke-dasharray:2px, 3px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- The Shield -->
|
||||||
|
<g transform="translate(340, 240)" style="fill:rgb(0, 0, 0);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
|
||||||
|
<!-- Shield outer glow ring -->
|
||||||
|
<path d="M 0 -130 L 95 -95 L 95 30 Q 95 95 0 130 Q -95 95 -95 30 L -95 -95 Z" fill="none" stroke="#d70751" stroke-width="2" opacity="0.4" style="fill:none;stroke:rgb(215, 7, 81);color:rgb(0, 0, 0);stroke-width:2px;stroke-linecap:butt;stroke-linejoin:miter;opacity:0.4;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||||
|
<!-- Shield body -->
|
||||||
|
<path d="M 0 -120 L 88 -88 L 88 28 Q 88 88 0 120 Q -88 88 -88 28 L -88 -88 Z" fill="#a30640" stroke="#d70751" stroke-width="1.5" style="fill:rgb(163, 6, 64);stroke:rgb(215, 7, 81);color:rgb(0, 0, 0);stroke-width:1.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||||
|
<!-- Inner shield panel (terminal screen) -->
|
||||||
|
<rect x="-78" y="-78" width="156" height="156" rx="4" fill="#0a1320" stroke="#2a3a5a" stroke-width="1" style="fill:rgb(10, 19, 32);stroke:rgb(42, 58, 90);color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Terminal content inside shield -->
|
||||||
|
<g clip-path="url(#screenClip)" style="fill:rgb(0, 0, 0);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
|
||||||
|
<!-- Terminal title bar -->
|
||||||
|
<rect x="262" y="162" width="156" height="14" fill="#1a2942" style="fill:rgb(26, 41, 66);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||||
|
<circle cx="270" cy="169" r="2.5" fill="#ff6b6b" style="fill:rgb(255, 107, 107);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||||
|
<circle cx="278" cy="169" r="2.5" fill="#ffd166" style="fill:rgb(255, 209, 102);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||||
|
<circle cx="286" cy="169" r="2.5" fill="#7fffaf" style="fill:rgb(127, 255, 175);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||||
|
<text x="345" y="172" text-anchor="middle" font-family="ui-monospace, monospace" font-size="8" fill="#8a9ab8" style="fill:rgb(138, 154, 184);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:ui-monospace, monospace;font-size:8px;font-weight:400;text-anchor:middle;dominant-baseline:auto">debian — unattended-upgrades</text>
|
||||||
|
|
||||||
|
<!-- Code lines -->
|
||||||
|
<text x="268" y="190" style="fill:rgb(255, 209, 102);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:ui-monospace, monospace;font-size:11px;font-weight:400;text-anchor:start;dominant-baseline:auto">root@srv:~#</text>
|
||||||
|
<text x="338" y="190" style="fill:rgb(127, 255, 175);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:ui-monospace, monospace;font-size:11px;font-weight:400;text-anchor:start;dominant-baseline:auto">apt update</text>
|
||||||
|
|
||||||
|
<text x="268" y="204" style="fill:rgb(90, 138, 122);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:ui-monospace, monospace;font-size:11px;font-weight:400;text-anchor:start;dominant-baseline:auto"># Reading package lists...</text>
|
||||||
|
|
||||||
|
<text x="268" y="218" style="fill:rgb(255, 209, 102);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:ui-monospace, monospace;font-size:11px;font-weight:400;text-anchor:start;dominant-baseline:auto">root@srv:~#</text>
|
||||||
|
<text x="338" y="218" style="fill:rgb(127, 255, 175);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:ui-monospace, monospace;font-size:11px;font-weight:400;text-anchor:start;dominant-baseline:auto">unattended-upgrade</text>
|
||||||
|
|
||||||
|
<text x="268" y="232" style="fill:rgb(90, 138, 122);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:ui-monospace, monospace;font-size:11px;font-weight:400;text-anchor:start;dominant-baseline:auto"># Checking security archive</text>
|
||||||
|
|
||||||
|
<text x="268" y="246" style="fill:rgb(127, 255, 175);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:ui-monospace, monospace;font-size:11px;font-weight:400;text-anchor:start;dominant-baseline:auto">→ openssl 3.0.15</text>
|
||||||
|
<text x="268" y="260" style="fill:rgb(127, 255, 175);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:ui-monospace, monospace;font-size:11px;font-weight:400;text-anchor:start;dominant-baseline:auto">→ linux-image 6.1.140</text>
|
||||||
|
<text x="268" y="274" style="fill:rgb(127, 255, 175);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:ui-monospace, monospace;font-size:11px;font-weight:400;text-anchor:start;dominant-baseline:auto">→ libpam 1.5.2-6+</text>
|
||||||
|
|
||||||
|
<text x="268" y="290" style="fill:rgb(90, 138, 122);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:ui-monospace, monospace;font-size:11px;font-weight:400;text-anchor:start;dominant-baseline:auto"># 3 packages upgraded ✓</text>
|
||||||
|
|
||||||
|
<text x="268" y="308" style="fill:rgb(255, 209, 102);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:ui-monospace, monospace;font-size:11px;font-weight:400;text-anchor:start;dominant-baseline:auto">root@srv:~#</text>
|
||||||
|
<text x="338" y="308" style="fill:rgb(127, 255, 175);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:ui-monospace, monospace;font-size:11px;font-weight:400;text-anchor:start;dominant-baseline:auto">_</text>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Lock icon at top of shield -->
|
||||||
|
<g transform="translate(340, 130)" style="fill:rgb(0, 0, 0);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
|
||||||
|
<rect x="-12" y="-2" width="24" height="20" rx="3" fill="#ffd166" stroke="#a8861f" stroke-width="1" style="fill:rgb(255, 209, 102);stroke:rgb(168, 134, 31);color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||||
|
<path d="M -8 -2 L -8 -10 Q -8 -18 0 -18 Q 8 -18 8 -10 L 8 -2" fill="none" stroke="#ffd166" stroke-width="2.5" mask="url(#imagine-text-gaps-g4glik)" style="fill:none;stroke:rgb(255, 209, 102);color:rgb(0, 0, 0);stroke-width:2.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||||
|
<circle cx="0" cy="7" r="2.5" fill="#1a2942" style="fill:rgb(26, 41, 66);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||||
|
<rect x="-1" y="7" width="2" height="6" fill="#1a2942" style="fill:rgb(26, 41, 66);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Debian swirl emblem (stylized) -->
|
||||||
|
<g transform="translate(340, 358)" opacity="0.9" style="fill:rgb(0, 0, 0);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:0.9;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
|
||||||
|
<circle cx="0" cy="0" r="22" fill="none" stroke="#d70751" stroke-width="2" style="fill:none;stroke:rgb(215, 7, 81);color:rgb(0, 0, 0);stroke-width:2px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||||
|
<path d="M -8 -12 Q -16 -4 -12 6 Q -4 14 8 10 Q 14 4 12 -4" fill="none" stroke="#d70751" stroke-width="2.5" stroke-linecap="round" mask="url(#imagine-text-gaps-g4glik)" style="fill:none;stroke:rgb(215, 7, 81);color:rgb(0, 0, 0);stroke-width:2.5px;stroke-linecap:round;stroke-linejoin:miter;opacity:1;font-family:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Title -->
|
||||||
|
<text x="340" y="40" text-anchor="middle" font-family="var(--font-sans), system-ui, sans-serif" font-size="20" font-weight="500" fill="#f5f7fa" style="fill:rgb(245, 247, 250);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:Anthropic Sans, sans-serif, system-ui, sans-serif;font-size:20px;font-weight:500;text-anchor:middle;dominant-baseline:auto">
|
||||||
|
Mises à jour automatiques de sécurité
|
||||||
|
</text>
|
||||||
|
<text x="340" y="62" text-anchor="middle" font-family="var(--font-sans), system-ui, sans-serif" font-size="13" fill="#a8b8d0" style="fill:rgb(168, 184, 208);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:Anthropic Sans, sans-serif, system-ui, sans-serif;font-size:13px;font-weight:400;text-anchor:middle;dominant-baseline:auto">
|
||||||
|
Debian · unattended-upgrades
|
||||||
|
</text>
|
||||||
|
|
||||||
|
<!-- Bottom tagline -->
|
||||||
|
<text x="340" y="420" text-anchor="middle" font-family="ui-monospace, monospace" font-size="11" fill="#7fffaf" opacity="0.85" style="fill:rgb(127, 255, 175);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:0.85;font-family:ui-monospace, monospace;font-size:11px;font-weight:400;text-anchor:middle;dominant-baseline:auto">
|
||||||
|
apt · security.debian.org · CVE patches
|
||||||
|
</text>
|
||||||
|
<text x="340" y="440" text-anchor="middle" font-family="var(--font-sans), system-ui, sans-serif" font-size="11" fill="#6b7a99" font-style="italic" style="fill:rgb(107, 122, 153);stroke:none;color:rgb(0, 0, 0);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:Anthropic Sans, sans-serif, system-ui, sans-serif;font-size:11px;font-weight:400;font-style:italic;text-anchor:middle;dominant-baseline:auto">
|
||||||
|
Un serveur protégé pendant que vous dormez
|
||||||
|
</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 31 KiB |
@@ -1,86 +1,155 @@
|
|||||||
Dans un environnement de serveur ou de poste de travail, maintenir un système à jour avec les derniers correctifs de sécurité est crucial pour éviter toute vulnérabilité. Debian, l'une des distributions Linux les plus populaires et stables, propose des mécanismes permettant d'automatiser ce processus de mise à jour, et ainsi garantir que les correctifs de sécurité sont appliqués sans nécessiter une intervention manuelle.
|
Maintenir un système Debian à jour, c'est un peu comme fermer ses fenêtres avant de partir en vacances : on sait qu'il faut le faire, on sait pourquoi, et pourtant ça finit régulièrement par passer à la trappe. Le problème, c'est qu'une CVE qui traîne plusieurs semaines sur un serveur exposé, ça ne pardonne pas toujours.
|
||||||
|
|
||||||
Comment activer les mises à jour automatiques sur une machine Debian, en se concentrant spécifiquement sur les correctifs de sécurité ? Comment configurer ce processus pour un contrôle fin et comment tester que tout fonctionne correctement ?
|
Heureusement, Debian fournit tout ce qu'il faut pour automatiser l'application des correctifs de sécurité, à travers un paquet qui s'appelle `unattended-upgrades`. L'idée est simple : on configure une fois, et la machine se débrouille pour appliquer les patches `-security` sans qu'on ait à y penser. Ce qui suit, c'est la marche à suivre, avec les pièges que j'ai croisés en route.
|
||||||
|
|
||||||
### Étapes pour activer les mises à jour automatiques
|
## Le principe
|
||||||
|
|
||||||
#### 1. Installer les paquets nécessaires
|
`unattended-upgrades` n'est pas un démon qui tourne en permanence. C'est un script qui est lancé une fois par jour par un timer systemd (`apt-daily-upgrade.timer`), et qui regarde dans sa configuration quelles « origines » de paquets il a le droit de mettre à jour. Par défaut, il est conservateur : il ne touche qu'aux paquets venant du dépôt de sécurité officiel. C'est exactement ce qu'on veut sur un serveur de production.
|
||||||
|
|
||||||
La première étape pour activer les mises à jour automatiques sur Debian consiste à installer les outils nécessaires. Cela inclut le paquet `unattended-upgrades`, qui permet de gérer les mises à jour automatiques, ainsi que `apt-listchanges` qui permet de recevoir des informations sur les paquets mis à jour.
|
Deux fichiers entrent en jeu :
|
||||||
|
|
||||||
Exécutez la commande suivante pour installer ces paquets :
|
- `/etc/apt/apt.conf.d/20auto-upgrades` décide *si* et *à quelle fréquence* unattended-upgrades est exécuté
|
||||||
|
- `/etc/apt/apt.conf.d/50unattended-upgrades` décide *quoi* mettre à jour et *comment*
|
||||||
|
|
||||||
|
On va passer par les deux.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Rien de très exotique :
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo apt update
|
sudo apt update
|
||||||
sudo apt install -y unattended-upgrades apt-listchanges
|
sudo apt install -y unattended-upgrades apt-listchanges
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 2. Configurer `unattended-upgrades`
|
`apt-listchanges` n'est pas strictement nécessaire, mais il est utile : il permet de recevoir un résumé des changements appliqués (notamment les entrées de `NEWS.Debian` qui annoncent parfois des changements de comportement qu'il vaut mieux ne pas découvrir un lundi matin).
|
||||||
|
|
||||||
Une fois le paquet installé, vous devez configurer `unattended-upgrades` pour qu'il applique automatiquement les mises à jour de sécurité. Cela se fait en modifiant certains fichiers de configuration dans `/etc/apt/apt.conf.d/`.
|
## Activer l'exécution automatique
|
||||||
|
|
||||||
Exécutez la commande suivante pour reconfigurer `unattended-upgrades` :
|
La commande classique pour ça :
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo dpkg-reconfigure --priority=low unattended-upgrades
|
sudo dpkg-reconfigure --priority=low unattended-upgrades
|
||||||
```
|
```
|
||||||
|
|
||||||
Lors de la configuration, vous serez invité à activer les mises à jour automatiques. Assurez-vous que l'option **"Install security updates"** est sélectionnée. Cela garantit que les mises à jour de sécurité seront appliquées automatiquement sans intervention manuelle.
|
L'interface te pose une question simple — « voulez-vous appliquer automatiquement les mises à jour de stabilité ? » — réponds oui. En coulisses, cette commande crée le fichier `/etc/apt/apt.conf.d/20auto-upgrades` avec ce contenu :
|
||||||
|
|
||||||
#### 3. Vérifier la configuration dans `/etc/apt/apt.conf.d/50unattended-upgrades`
|
```
|
||||||
|
APT::Periodic::Update-Package-Lists "1";
|
||||||
Une fois la configuration initiale terminée, il est important de vérifier que le fichier de configuration principal de `unattended-upgrades` permet bien l'installation des mises à jour de sécurité.
|
APT::Periodic::Unattended-Upgrade "1";
|
||||||
|
|
||||||
Éditez le fichier `/etc/apt/apt.conf.d/50unattended-upgrades` avec votre éditeur de texte préféré, par exemple `nano` :
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo nano /etc/apt/apt.conf.d/50unattended-upgrades
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Dans ce fichier, vous devez vous assurer que la ligne suivante est présente et non commentée :
|
Le `"1"` signifie « tous les jours ». Tu peux mettre `"7"` pour hebdomadaire, mais sur un serveur exposé je ne vois pas l'intérêt de retarder.
|
||||||
|
|
||||||
```plaintext
|
Pendant qu'on y est, deux options qu'on ajoute souvent à ce fichier :
|
||||||
"${distro_id}:${distro_codename}-security";
|
|
||||||
|
```
|
||||||
|
APT::Periodic::Download-Upgradeable-Packages "1";
|
||||||
|
APT::Periodic::AutocleanInterval "7";
|
||||||
```
|
```
|
||||||
|
|
||||||
Cela garantit que les mises à jour de sécurité de votre version de Debian seront appliquées automatiquement.
|
La première télécharge les paquets en amont, ce qui rend l'installation plus rapide le moment venu. La seconde nettoie le cache `/var/cache/apt/archives` une fois par semaine, ce qui évite que ce dossier ne grossisse indéfiniment.
|
||||||
|
|
||||||
**Optionnel** : Si vous souhaitez également appliquer des mises à jour non critiques (par exemple des mises à jour mineures ou des corrections de bugs), vous pouvez activer les mises à jour pour tous les paquets en dé-commentant ou ajoutant cette ligne :
|
## Configurer le périmètre des mises à jour
|
||||||
|
|
||||||
```plaintext
|
C'est dans `/etc/apt/apt.conf.d/50unattended-upgrades` que ça se joue. Le fichier est livré commenté et documenté, ce qui aide. La section la plus importante ressemble à ça :
|
||||||
"${distro_id}:${distro_codename}-updates";
|
|
||||||
|
```
|
||||||
|
Unattended-Upgrade::Origins-Pattern {
|
||||||
|
"origin=Debian,codename=${distro_codename}-updates";
|
||||||
|
"origin=Debian,codename=${distro_codename},label=Debian";
|
||||||
|
"origin=Debian,codename=${distro_codename},label=Debian-Security";
|
||||||
|
"origin=Debian,codename=${distro_codename}-security,label=Debian-Security";
|
||||||
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
Cependant, cette option peut introduire des mises à jour qui ne sont pas toujours aussi urgentes que celles de sécurité. Il est donc recommandé de ne l'activer que si vous êtes à l'aise avec des mises à jour supplémentaires automatiques.
|
Sur une Debian récente (Bookworm et au-delà), ces lignes sont décommentées par défaut et couvrent les mises à jour de sécurité. Si tu veux être strictement sécurité-seulement, garde uniquement les deux lignes contenant `Debian-Security` et commente les autres. La règle est : `label=Debian-Security` correspond aux correctifs de l'équipe sécurité, le reste correspond aux mises à jour de point release (les `12.6 → 12.7` et compagnie).
|
||||||
|
|
||||||
#### 4. Tester la configuration des mises à jour automatiques
|
Quelques options supplémentaires qui méritent qu'on s'y arrête :
|
||||||
|
|
||||||
Pour vous assurer que `unattended-upgrades` fonctionne comme prévu, il est utile de faire un test en mode "dry-run". Cela permet de simuler les mises à jour sans les appliquer réellement.
|
### Le redémarrage automatique
|
||||||
|
|
||||||
Exécutez la commande suivante :
|
```
|
||||||
|
Unattended-Upgrade::Automatic-Reboot "false";
|
||||||
|
Unattended-Upgrade::Automatic-Reboot-Time "02:00";
|
||||||
|
```
|
||||||
|
|
||||||
|
Question politique autant que technique. Certaines mises à jour — typiquement le noyau ou la glibc — ne prennent effet qu'après reboot. Tant que la machine n'a pas redémarré, le correctif n'est pas réellement appliqué, même si le paquet est installé. Tu as deux options : laisser à `"false"` et reboot manuellement quand tu veux (en surveillant `/var/run/reboot-required`), ou passer à `"true"` avec une heure creuse. Sur un serveur isolé, le reboot auto se défend. Sur une base de données critique, beaucoup moins.
|
||||||
|
|
||||||
|
### Les notifications
|
||||||
|
|
||||||
|
```
|
||||||
|
Unattended-Upgrade::Mail "admin@exemple.fr";
|
||||||
|
Unattended-Upgrade::MailReport "on-change";
|
||||||
|
```
|
||||||
|
|
||||||
|
Très utile. Tu reçois un mail uniquement quand quelque chose a été installé (ou a échoué), ce qui évite le bruit. Évidemment, il faut un MTA configuré — `msmtp` ou un postfix en relais SMTP font le boulot.
|
||||||
|
|
||||||
|
### Le nettoyage
|
||||||
|
|
||||||
|
```
|
||||||
|
Unattended-Upgrade::Remove-Unused-Kernel-Packages "true";
|
||||||
|
Unattended-Upgrade::Remove-Unused-Dependencies "true";
|
||||||
|
```
|
||||||
|
|
||||||
|
Sans ça, les vieux noyaux s'accumulent dans `/boot`, qui finit par se remplir, ce qui finit par bloquer la prochaine mise à jour du noyau. Classique.
|
||||||
|
|
||||||
|
## Tester avant de laisser tourner
|
||||||
|
|
||||||
|
Avant de partir en weekend, vérifie que la configuration est bien comprise :
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo unattended-upgrades --dry-run --debug
|
sudo unattended-upgrades --dry-run --debug
|
||||||
```
|
```
|
||||||
|
|
||||||
Cette commande simule le processus de mise à jour et affiche des informations détaillées sur les paquets qui seraient mis à jour. Cela vous permet de vérifier que la configuration fonctionne correctement et que seules les mises à jour de sécurité (ou celles que vous avez spécifiées) seront prises en compte.
|
Le dry-run ne touche à rien, mais il liste les paquets qui seraient installés et — surtout — il indique pourquoi un paquet est ou n'est pas retenu. Si tu vois `Checking: <paquet>` suivi de `Allowed origins are:` et que ton dépôt de sécurité apparaît bien dans la liste, c'est bon signe.
|
||||||
|
|
||||||
#### 5. Vérifier les logs
|
Tu peux aussi vérifier que les timers systemd sont actifs :
|
||||||
|
|
||||||
Une fois les mises à jour automatiques activées et en cours d'exécution, il est important de vérifier régulièrement les logs pour vous assurer qu'aucune mise à jour n'a échoué. Les logs de `unattended-upgrades` sont stockés dans le fichier `/var/log/unattended-upgrades/unattended-upgrades.log`.
|
|
||||||
|
|
||||||
Pour consulter les logs, utilisez la commande suivante :
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo cat /var/log/unattended-upgrades/unattended-upgrades.log
|
systemctl list-timers apt-daily.timer apt-daily-upgrade.timer
|
||||||
```
|
```
|
||||||
|
|
||||||
Cela vous permet de suivre les mises à jour installées automatiquement et d’identifier tout problème éventuel.
|
Ces deux timers sont fournis par le paquet `apt` lui-même, indépendamment de unattended-upgrades. Le premier rafraîchit la liste des paquets, le second déclenche l'upgrade. S'ils sont marqués `inactive`, c'est qu'ils sont masqués quelque part — un `systemctl unmask` règle généralement le problème.
|
||||||
|
|
||||||
---
|
## Vérifier après coup
|
||||||
|
|
||||||
En activant les mises à jour automatiques sur votre machine Debian, vous vous assurez que les correctifs de sécurité sont appliqués rapidement et sans intervention manuelle, réduisant ainsi le risque de vulnérabilités exploitables.
|
Les logs vivent ici :
|
||||||
|
|
||||||
N'oubliez pas que bien que l'activation des mises à jour automatiques pour les correctifs de sécurité soit essentielle, il est également important de tester régulièrement le processus, de vérifier les logs, et de maintenir une vigilance pour toute mise à jour système majeure qui pourrait nécessiter une attention spéciale.
|
```bash
|
||||||
|
sudo less /var/log/unattended-upgrades/unattended-upgrades.log
|
||||||
|
sudo less /var/log/unattended-upgrades/unattended-upgrades-dpkg.log
|
||||||
|
```
|
||||||
|
|
||||||
En appliquant cette configuration, vous minimisez les risques liés à des failles de sécurité tout en maintenant la stabilité et la sécurité de votre environnement Debian.
|
Le premier est un résumé lisible, le second contient la sortie brute de `dpkg`. Quand quelque chose se passe mal, c'est généralement dans le second qu'on trouve l'explication (un fichier de configuration modifié, un service qui refuse de redémarrer, ce genre de choses).
|
||||||
|
|
||||||
|
Pour savoir si la machine attend un reboot après une mise à jour de noyau :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ls /var/run/reboot-required 2>/dev/null && cat /var/run/reboot-required.pkgs
|
||||||
|
```
|
||||||
|
|
||||||
|
Si le fichier existe, c'est qu'au moins un paquet installé recommande un redémarrage. Le second fichier liste lesquels.
|
||||||
|
|
||||||
|
## Quelques pièges
|
||||||
|
|
||||||
|
**Les fichiers de configuration modifiés.** Si tu as personnalisé un `/etc/...` dont le paquet propose une nouvelle version, unattended-upgrades va voir un conflit et laisser tomber l'installation par défaut (option `--force-confdef`). C'est généralement le bon comportement, mais ça veut dire que certaines mises à jour resteront en attente jusqu'à une intervention manuelle. Les logs te le diront.
|
||||||
|
|
||||||
|
**Les paquets blacklistés.** Tu peux exclure des paquets précis de la mise à jour automatique :
|
||||||
|
|
||||||
|
```
|
||||||
|
Unattended-Upgrade::Package-Blacklist {
|
||||||
|
"libc6";
|
||||||
|
"linux-image-.*";
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
À utiliser avec discernement — exclure libc6 ou le noyau, c'est précisément exclure les correctifs qui comptent le plus. Mais sur une machine où un reboot coûte cher, ça permet de garder le contrôle sur les paquets sensibles tout en automatisant le reste.
|
||||||
|
|
||||||
|
**Les dépôts tiers.** Par défaut, seuls les dépôts Debian officiels sont concernés. Si tu utilises un PPA ou un dépôt comme celui de Docker ou de PostgreSQL, il faut explicitement l'ajouter dans `Origins-Pattern`. Sinon, ces paquets ne sont jamais mis à jour automatiquement — ce qui peut être un piège silencieux.
|
||||||
|
|
||||||
|
## Pour conclure
|
||||||
|
|
||||||
|
L'automatisation des mises à jour de sécurité sur Debian, ce n'est pas une boîte noire : c'est un script qui tourne une fois par jour, qui lit deux fichiers texte, et qui appelle `apt` avec des règles bien définies. Une fois qu'on a compris ça, le configurer revient à éditer ces deux fichiers selon ses contraintes — niveau de risque, fenêtre de reboot acceptable, notifications souhaitées.
|
||||||
|
|
||||||
|
Le minimum vital sur un serveur exposé tient en quatre points : `unattended-upgrades` installé, le périmètre limité aux dépôts `-security`, les notifications mail activées, et un coup d'œil régulier à `/var/run/reboot-required` pour ne pas oublier les redémarrages. Le reste, c'est de l'ajustement selon le contexte.
|
||||||
@@ -1,13 +1,36 @@
|
|||||||
{
|
{
|
||||||
"uuid": "976fd7f0-e53d-44e2-a879-58194765f3cf",
|
"uuid": "976fd7f0-e53d-44e2-a879-58194765f3cf",
|
||||||
"slug": "activer-les-mises-a-jour-automatiques-sur-debian-pour-une-gestion-simplifiee-des-correctifs-de-securite",
|
"slug": "mises-a-jour-automatiques-de-securite-sur-debian",
|
||||||
"title": "Activer les mises à jour automatiques sur Debian pour une gestion simplifiée des correctifs de sécurité",
|
"title": "Mises à jour automatiques de sécurité sur Debian",
|
||||||
"author": "cedric@abonnel.fr",
|
"author": "cedric@abonnel.fr",
|
||||||
"published": true,
|
"published": true,
|
||||||
"published_at": "2026-01-06 20:45:52",
|
"published_at": "2026-01-06 20:45",
|
||||||
"created_at": "2026-01-06 20:45:52",
|
"created_at": "2026-01-06 20:45:52",
|
||||||
"updated_at": "2026-01-06 20:45:52",
|
"updated_at": "2026-05-12 00:22:53",
|
||||||
"revisions": [],
|
"revisions": [
|
||||||
"cover": "cover.jpg",
|
{
|
||||||
|
"n": 1,
|
||||||
|
"date": "2026-05-12 00:18:42",
|
||||||
|
"comment": "",
|
||||||
|
"title": "Mises à jour automatiques de sécurité sur Debian"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"n": 2,
|
||||||
|
"date": "2026-05-12 00:22:53",
|
||||||
|
"comment": "",
|
||||||
|
"title": "Mises à jour automatiques de sécurité sur Debian"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cover": "cover.svg",
|
||||||
|
"files_meta": {
|
||||||
|
"5cfac9bc6078c2d1-31391.svg": {
|
||||||
|
"author": "Cédrix",
|
||||||
|
"source_url": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"external_links": [],
|
||||||
|
"seo_title": "",
|
||||||
|
"seo_description": "",
|
||||||
|
"og_image": "",
|
||||||
"category": "linux"
|
"category": "linux"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,155 @@
|
|||||||
|
Maintenir un système Debian à jour, c'est un peu comme fermer ses fenêtres avant de partir en vacances : on sait qu'il faut le faire, on sait pourquoi, et pourtant ça finit régulièrement par passer à la trappe. Le problème, c'est qu'une CVE qui traîne plusieurs semaines sur un serveur exposé, ça ne pardonne pas toujours.
|
||||||
|
|
||||||
|
Heureusement, Debian fournit tout ce qu'il faut pour automatiser l'application des correctifs de sécurité, à travers un paquet qui s'appelle `unattended-upgrades`. L'idée est simple : on configure une fois, et la machine se débrouille pour appliquer les patches `-security` sans qu'on ait à y penser. Ce qui suit, c'est la marche à suivre, avec les pièges que j'ai croisés en route.
|
||||||
|
|
||||||
|
## Le principe
|
||||||
|
|
||||||
|
`unattended-upgrades` n'est pas un démon qui tourne en permanence. C'est un script qui est lancé une fois par jour par un timer systemd (`apt-daily-upgrade.timer`), et qui regarde dans sa configuration quelles « origines » de paquets il a le droit de mettre à jour. Par défaut, il est conservateur : il ne touche qu'aux paquets venant du dépôt de sécurité officiel. C'est exactement ce qu'on veut sur un serveur de production.
|
||||||
|
|
||||||
|
Deux fichiers entrent en jeu :
|
||||||
|
|
||||||
|
- `/etc/apt/apt.conf.d/20auto-upgrades` décide *si* et *à quelle fréquence* unattended-upgrades est exécuté
|
||||||
|
- `/etc/apt/apt.conf.d/50unattended-upgrades` décide *quoi* mettre à jour et *comment*
|
||||||
|
|
||||||
|
On va passer par les deux.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Rien de très exotique :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install -y unattended-upgrades apt-listchanges
|
||||||
|
```
|
||||||
|
|
||||||
|
`apt-listchanges` n'est pas strictement nécessaire, mais il est utile : il permet de recevoir un résumé des changements appliqués (notamment les entrées de `NEWS.Debian` qui annoncent parfois des changements de comportement qu'il vaut mieux ne pas découvrir un lundi matin).
|
||||||
|
|
||||||
|
## Activer l'exécution automatique
|
||||||
|
|
||||||
|
La commande classique pour ça :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo dpkg-reconfigure --priority=low unattended-upgrades
|
||||||
|
```
|
||||||
|
|
||||||
|
L'interface te pose une question simple — « voulez-vous appliquer automatiquement les mises à jour de stabilité ? » — réponds oui. En coulisses, cette commande crée le fichier `/etc/apt/apt.conf.d/20auto-upgrades` avec ce contenu :
|
||||||
|
|
||||||
|
```
|
||||||
|
APT::Periodic::Update-Package-Lists "1";
|
||||||
|
APT::Periodic::Unattended-Upgrade "1";
|
||||||
|
```
|
||||||
|
|
||||||
|
Le `"1"` signifie « tous les jours ». Tu peux mettre `"7"` pour hebdomadaire, mais sur un serveur exposé je ne vois pas l'intérêt de retarder.
|
||||||
|
|
||||||
|
Pendant qu'on y est, deux options qu'on ajoute souvent à ce fichier :
|
||||||
|
|
||||||
|
```
|
||||||
|
APT::Periodic::Download-Upgradeable-Packages "1";
|
||||||
|
APT::Periodic::AutocleanInterval "7";
|
||||||
|
```
|
||||||
|
|
||||||
|
La première télécharge les paquets en amont, ce qui rend l'installation plus rapide le moment venu. La seconde nettoie le cache `/var/cache/apt/archives` une fois par semaine, ce qui évite que ce dossier ne grossisse indéfiniment.
|
||||||
|
|
||||||
|
## Configurer le périmètre des mises à jour
|
||||||
|
|
||||||
|
C'est dans `/etc/apt/apt.conf.d/50unattended-upgrades` que ça se joue. Le fichier est livré commenté et documenté, ce qui aide. La section la plus importante ressemble à ça :
|
||||||
|
|
||||||
|
```
|
||||||
|
Unattended-Upgrade::Origins-Pattern {
|
||||||
|
"origin=Debian,codename=${distro_codename}-updates";
|
||||||
|
"origin=Debian,codename=${distro_codename},label=Debian";
|
||||||
|
"origin=Debian,codename=${distro_codename},label=Debian-Security";
|
||||||
|
"origin=Debian,codename=${distro_codename}-security,label=Debian-Security";
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Sur une Debian récente (Bookworm et au-delà), ces lignes sont décommentées par défaut et couvrent les mises à jour de sécurité. Si tu veux être strictement sécurité-seulement, garde uniquement les deux lignes contenant `Debian-Security` et commente les autres. La règle est : `label=Debian-Security` correspond aux correctifs de l'équipe sécurité, le reste correspond aux mises à jour de point release (les `12.6 → 12.7` et compagnie).
|
||||||
|
|
||||||
|
Quelques options supplémentaires qui méritent qu'on s'y arrête :
|
||||||
|
|
||||||
|
### Le redémarrage automatique
|
||||||
|
|
||||||
|
```
|
||||||
|
Unattended-Upgrade::Automatic-Reboot "false";
|
||||||
|
Unattended-Upgrade::Automatic-Reboot-Time "02:00";
|
||||||
|
```
|
||||||
|
|
||||||
|
Question politique autant que technique. Certaines mises à jour — typiquement le noyau ou la glibc — ne prennent effet qu'après reboot. Tant que la machine n'a pas redémarré, le correctif n'est pas réellement appliqué, même si le paquet est installé. Tu as deux options : laisser à `"false"` et reboot manuellement quand tu veux (en surveillant `/var/run/reboot-required`), ou passer à `"true"` avec une heure creuse. Sur un serveur isolé, le reboot auto se défend. Sur une base de données critique, beaucoup moins.
|
||||||
|
|
||||||
|
### Les notifications
|
||||||
|
|
||||||
|
```
|
||||||
|
Unattended-Upgrade::Mail "admin@exemple.fr";
|
||||||
|
Unattended-Upgrade::MailReport "on-change";
|
||||||
|
```
|
||||||
|
|
||||||
|
Très utile. Tu reçois un mail uniquement quand quelque chose a été installé (ou a échoué), ce qui évite le bruit. Évidemment, il faut un MTA configuré — `msmtp` ou un postfix en relais SMTP font le boulot.
|
||||||
|
|
||||||
|
### Le nettoyage
|
||||||
|
|
||||||
|
```
|
||||||
|
Unattended-Upgrade::Remove-Unused-Kernel-Packages "true";
|
||||||
|
Unattended-Upgrade::Remove-Unused-Dependencies "true";
|
||||||
|
```
|
||||||
|
|
||||||
|
Sans ça, les vieux noyaux s'accumulent dans `/boot`, qui finit par se remplir, ce qui finit par bloquer la prochaine mise à jour du noyau. Classique.
|
||||||
|
|
||||||
|
## Tester avant de laisser tourner
|
||||||
|
|
||||||
|
Avant de partir en weekend, vérifie que la configuration est bien comprise :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo unattended-upgrades --dry-run --debug
|
||||||
|
```
|
||||||
|
|
||||||
|
Le dry-run ne touche à rien, mais il liste les paquets qui seraient installés et — surtout — il indique pourquoi un paquet est ou n'est pas retenu. Si tu vois `Checking: <paquet>` suivi de `Allowed origins are:` et que ton dépôt de sécurité apparaît bien dans la liste, c'est bon signe.
|
||||||
|
|
||||||
|
Tu peux aussi vérifier que les timers systemd sont actifs :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
systemctl list-timers apt-daily.timer apt-daily-upgrade.timer
|
||||||
|
```
|
||||||
|
|
||||||
|
Ces deux timers sont fournis par le paquet `apt` lui-même, indépendamment de unattended-upgrades. Le premier rafraîchit la liste des paquets, le second déclenche l'upgrade. S'ils sont marqués `inactive`, c'est qu'ils sont masqués quelque part — un `systemctl unmask` règle généralement le problème.
|
||||||
|
|
||||||
|
## Vérifier après coup
|
||||||
|
|
||||||
|
Les logs vivent ici :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo less /var/log/unattended-upgrades/unattended-upgrades.log
|
||||||
|
sudo less /var/log/unattended-upgrades/unattended-upgrades-dpkg.log
|
||||||
|
```
|
||||||
|
|
||||||
|
Le premier est un résumé lisible, le second contient la sortie brute de `dpkg`. Quand quelque chose se passe mal, c'est généralement dans le second qu'on trouve l'explication (un fichier de configuration modifié, un service qui refuse de redémarrer, ce genre de choses).
|
||||||
|
|
||||||
|
Pour savoir si la machine attend un reboot après une mise à jour de noyau :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ls /var/run/reboot-required 2>/dev/null && cat /var/run/reboot-required.pkgs
|
||||||
|
```
|
||||||
|
|
||||||
|
Si le fichier existe, c'est qu'au moins un paquet installé recommande un redémarrage. Le second fichier liste lesquels.
|
||||||
|
|
||||||
|
## Quelques pièges
|
||||||
|
|
||||||
|
**Les fichiers de configuration modifiés.** Si tu as personnalisé un `/etc/...` dont le paquet propose une nouvelle version, unattended-upgrades va voir un conflit et laisser tomber l'installation par défaut (option `--force-confdef`). C'est généralement le bon comportement, mais ça veut dire que certaines mises à jour resteront en attente jusqu'à une intervention manuelle. Les logs te le diront.
|
||||||
|
|
||||||
|
**Les paquets blacklistés.** Tu peux exclure des paquets précis de la mise à jour automatique :
|
||||||
|
|
||||||
|
```
|
||||||
|
Unattended-Upgrade::Package-Blacklist {
|
||||||
|
"libc6";
|
||||||
|
"linux-image-.*";
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
À utiliser avec discernement — exclure libc6 ou le noyau, c'est précisément exclure les correctifs qui comptent le plus. Mais sur une machine où un reboot coûte cher, ça permet de garder le contrôle sur les paquets sensibles tout en automatisant le reste.
|
||||||
|
|
||||||
|
**Les dépôts tiers.** Par défaut, seuls les dépôts Debian officiels sont concernés. Si tu utilises un PPA ou un dépôt comme celui de Docker ou de PostgreSQL, il faut explicitement l'ajouter dans `Origins-Pattern`. Sinon, ces paquets ne sont jamais mis à jour automatiquement — ce qui peut être un piège silencieux.
|
||||||
|
|
||||||
|
## Pour conclure
|
||||||
|
|
||||||
|
L'automatisation des mises à jour de sécurité sur Debian, ce n'est pas une boîte noire : c'est un script qui tourne une fois par jour, qui lit deux fichiers texte, et qui appelle `apt` avec des règles bien définies. Une fois qu'on a compris ça, le configurer revient à éditer ces deux fichiers selon ses contraintes — niveau de risque, fenêtre de reboot acceptable, notifications souhaitées.
|
||||||
|
|
||||||
|
Le minimum vital sur un serveur exposé tient en quatre points : `unattended-upgrades` installé, le périmètre limité aux dépôts `-security`, les notifications mail activées, et un coup d'œil régulier à `/var/run/reboot-required` pour ne pas oublier les redémarrages. Le reste, c'est de l'ajustement selon le contexte.
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
Maintenir un système Debian à jour, c'est un peu comme fermer ses fenêtres avant de partir en vacances : on sait qu'il faut le faire, on sait pourquoi, et pourtant ça finit régulièrement par passer à la trappe. Le problème, c'est qu'une CVE qui traîne plusieurs semaines sur un serveur exposé, ça ne pardonne pas toujours.
|
||||||
|
|
||||||
|
Heureusement, Debian fournit tout ce qu'il faut pour automatiser l'application des correctifs de sécurité, à travers un paquet qui s'appelle `unattended-upgrades`. L'idée est simple : on configure une fois, et la machine se débrouille pour appliquer les patches `-security` sans qu'on ait à y penser. Ce qui suit, c'est la marche à suivre, avec les pièges que j'ai croisés en route.
|
||||||
|
|
||||||
|
## Le principe
|
||||||
|
|
||||||
|
`unattended-upgrades` n'est pas un démon qui tourne en permanence. C'est un script qui est lancé une fois par jour par un timer systemd (`apt-daily-upgrade.timer`), et qui regarde dans sa configuration quelles « origines » de paquets il a le droit de mettre à jour. Par défaut, il est conservateur : il ne touche qu'aux paquets venant du dépôt de sécurité officiel. C'est exactement ce qu'on veut sur un serveur de production.
|
||||||
|
|
||||||
|
Deux fichiers entrent en jeu :
|
||||||
|
|
||||||
|
- `/etc/apt/apt.conf.d/20auto-upgrades` décide *si* et *à quelle fréquence* unattended-upgrades est exécuté
|
||||||
|
- `/etc/apt/apt.conf.d/50unattended-upgrades` décide *quoi* mettre à jour et *comment*
|
||||||
|
|
||||||
|
On va passer par les deux.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Rien de très exotique :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install -y unattended-upgrades apt-listchanges
|
||||||
|
```
|
||||||
|
|
||||||
|
`apt-listchanges` n'est pas strictement nécessaire, mais il est utile : il permet de recevoir un résumé des changements appliqués (notamment les entrées de `NEWS.Debian` qui annoncent parfois des changements de comportement qu'il vaut mieux ne pas découvrir un lundi matin).
|
||||||
|
|
||||||
|
## Activer l'exécution automatique
|
||||||
|
|
||||||
|
La commande classique pour ça :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo dpkg-reconfigure --priority=low unattended-upgrades
|
||||||
|
```
|
||||||
|
|
||||||
|
L'interface te pose une question simple — « voulez-vous appliquer automatiquement les mises à jour de stabilité ? » — réponds oui. En coulisses, cette commande crée le fichier `/etc/apt/apt.conf.d/20auto-upgrades` avec ce contenu :
|
||||||
|
|
||||||
|
```
|
||||||
|
APT::Periodic::Update-Package-Lists "1";
|
||||||
|
APT::Periodic::Unattended-Upgrade "1";
|
||||||
|
```
|
||||||
|
|
||||||
|
Le `"1"` signifie « tous les jours ». Tu peux mettre `"7"` pour hebdomadaire, mais sur un serveur exposé je ne vois pas l'intérêt de retarder.
|
||||||
|
|
||||||
|
Pendant qu'on y est, deux options qu'on ajoute souvent à ce fichier :
|
||||||
|
|
||||||
|
```
|
||||||
|
APT::Periodic::Download-Upgradeable-Packages "1";
|
||||||
|
APT::Periodic::AutocleanInterval "7";
|
||||||
|
```
|
||||||
|
|
||||||
|
La première télécharge les paquets en amont, ce qui rend l'installation plus rapide le moment venu. La seconde nettoie le cache `/var/cache/apt/archives` une fois par semaine, ce qui évite que ce dossier ne grossisse indéfiniment.
|
||||||
|
|
||||||
|
## Configurer le périmètre des mises à jour
|
||||||
|
|
||||||
|
C'est dans `/etc/apt/apt.conf.d/50unattended-upgrades` que ça se joue. Le fichier est livré commenté et documenté, ce qui aide. La section la plus importante ressemble à ça :
|
||||||
|
|
||||||
|
```
|
||||||
|
Unattended-Upgrade::Origins-Pattern {
|
||||||
|
"origin=Debian,codename=${distro_codename}-updates";
|
||||||
|
"origin=Debian,codename=${distro_codename},label=Debian";
|
||||||
|
"origin=Debian,codename=${distro_codename},label=Debian-Security";
|
||||||
|
"origin=Debian,codename=${distro_codename}-security,label=Debian-Security";
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Sur une Debian récente (Bookworm et au-delà), ces lignes sont décommentées par défaut et couvrent les mises à jour de sécurité. Si tu veux être strictement sécurité-seulement, garde uniquement les deux lignes contenant `Debian-Security` et commente les autres. La règle est : `label=Debian-Security` correspond aux correctifs de l'équipe sécurité, le reste correspond aux mises à jour de point release (les `12.6 → 12.7` et compagnie).
|
||||||
|
|
||||||
|
Quelques options supplémentaires qui méritent qu'on s'y arrête :
|
||||||
|
|
||||||
|
### Le redémarrage automatique
|
||||||
|
|
||||||
|
```
|
||||||
|
Unattended-Upgrade::Automatic-Reboot "false";
|
||||||
|
Unattended-Upgrade::Automatic-Reboot-Time "02:00";
|
||||||
|
```
|
||||||
|
|
||||||
|
Question politique autant que technique. Certaines mises à jour — typiquement le noyau ou la glibc — ne prennent effet qu'après reboot. Tant que la machine n'a pas redémarré, le correctif n'est pas réellement appliqué, même si le paquet est installé. Tu as deux options : laisser à `"false"` et reboot manuellement quand tu veux (en surveillant `/var/run/reboot-required`), ou passer à `"true"` avec une heure creuse. Sur un serveur isolé, le reboot auto se défend. Sur une base de données critique, beaucoup moins.
|
||||||
|
|
||||||
|
### Les notifications
|
||||||
|
|
||||||
|
```
|
||||||
|
Unattended-Upgrade::Mail "admin@exemple.fr";
|
||||||
|
Unattended-Upgrade::MailReport "on-change";
|
||||||
|
```
|
||||||
|
|
||||||
|
Très utile. Tu reçois un mail uniquement quand quelque chose a été installé (ou a échoué), ce qui évite le bruit. Évidemment, il faut un MTA configuré — `msmtp` ou un postfix en relais SMTP font le boulot.
|
||||||
|
|
||||||
|
### Le nettoyage
|
||||||
|
|
||||||
|
```
|
||||||
|
Unattended-Upgrade::Remove-Unused-Kernel-Packages "true";
|
||||||
|
Unattended-Upgrade::Remove-Unused-Dependencies "true";
|
||||||
|
```
|
||||||
|
|
||||||
|
Sans ça, les vieux noyaux s'accumulent dans `/boot`, qui finit par se remplir, ce qui finit par bloquer la prochaine mise à jour du noyau. Classique.
|
||||||
|
|
||||||
|
## Tester avant de laisser tourner
|
||||||
|
|
||||||
|
Avant de partir en weekend, vérifie que la configuration est bien comprise :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo unattended-upgrades --dry-run --debug
|
||||||
|
```
|
||||||
|
|
||||||
|
Le dry-run ne touche à rien, mais il liste les paquets qui seraient installés et — surtout — il indique pourquoi un paquet est ou n'est pas retenu. Si tu vois `Checking: <paquet>` suivi de `Allowed origins are:` et que ton dépôt de sécurité apparaît bien dans la liste, c'est bon signe.
|
||||||
|
|
||||||
|
Tu peux aussi vérifier que les timers systemd sont actifs :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
systemctl list-timers apt-daily.timer apt-daily-upgrade.timer
|
||||||
|
```
|
||||||
|
|
||||||
|
Ces deux timers sont fournis par le paquet `apt` lui-même, indépendamment de unattended-upgrades. Le premier rafraîchit la liste des paquets, le second déclenche l'upgrade. S'ils sont marqués `inactive`, c'est qu'ils sont masqués quelque part — un `systemctl unmask` règle généralement le problème.
|
||||||
|
|
||||||
|
## Vérifier après coup
|
||||||
|
|
||||||
|
Les logs vivent ici :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo less /var/log/unattended-upgrades/unattended-upgrades.log
|
||||||
|
sudo less /var/log/unattended-upgrades/unattended-upgrades-dpkg.log
|
||||||
|
```
|
||||||
|
|
||||||
|
Le premier est un résumé lisible, le second contient la sortie brute de `dpkg`. Quand quelque chose se passe mal, c'est généralement dans le second qu'on trouve l'explication (un fichier de configuration modifié, un service qui refuse de redémarrer, ce genre de choses).
|
||||||
|
|
||||||
|
Pour savoir si la machine attend un reboot après une mise à jour de noyau :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ls /var/run/reboot-required 2>/dev/null && cat /var/run/reboot-required.pkgs
|
||||||
|
```
|
||||||
|
|
||||||
|
Si le fichier existe, c'est qu'au moins un paquet installé recommande un redémarrage. Le second fichier liste lesquels.
|
||||||
|
|
||||||
|
## Quelques pièges
|
||||||
|
|
||||||
|
**Les fichiers de configuration modifiés.** Si tu as personnalisé un `/etc/...` dont le paquet propose une nouvelle version, unattended-upgrades va voir un conflit et laisser tomber l'installation par défaut (option `--force-confdef`). C'est généralement le bon comportement, mais ça veut dire que certaines mises à jour resteront en attente jusqu'à une intervention manuelle. Les logs te le diront.
|
||||||
|
|
||||||
|
**Les paquets blacklistés.** Tu peux exclure des paquets précis de la mise à jour automatique :
|
||||||
|
|
||||||
|
```
|
||||||
|
Unattended-Upgrade::Package-Blacklist {
|
||||||
|
"libc6";
|
||||||
|
"linux-image-.*";
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
À utiliser avec discernement — exclure libc6 ou le noyau, c'est précisément exclure les correctifs qui comptent le plus. Mais sur une machine où un reboot coûte cher, ça permet de garder le contrôle sur les paquets sensibles tout en automatisant le reste.
|
||||||
|
|
||||||
|
**Les dépôts tiers.** Par défaut, seuls les dépôts Debian officiels sont concernés. Si tu utilises un PPA ou un dépôt comme celui de Docker ou de PostgreSQL, il faut explicitement l'ajouter dans `Origins-Pattern`. Sinon, ces paquets ne sont jamais mis à jour automatiquement — ce qui peut être un piège silencieux.
|
||||||
|
|
||||||
|
## Pour conclure
|
||||||
|
|
||||||
|
L'automatisation des mises à jour de sécurité sur Debian, ce n'est pas une boîte noire : c'est un script qui tourne une fois par jour, qui lit deux fichiers texte, et qui appelle `apt` avec des règles bien définies. Une fois qu'on a compris ça, le configurer revient à éditer ces deux fichiers selon ses contraintes — niveau de risque, fenêtre de reboot acceptable, notifications souhaitées.
|
||||||
|
|
||||||
|
Le minimum vital sur un serveur exposé tient en quatre points : `unattended-upgrades` installé, le périmètre limité aux dépôts `-security`, les notifications mail activées, et un coup d'œil régulier à `/var/run/reboot-required` pour ne pas oublier les redémarrages. Le reste, c'est de l'ajustement selon le contexte.
|
||||||
|
After Width: | Height: | Size: 620 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 65 KiB |
|
After Width: | Height: | Size: 49 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 144 KiB |
@@ -1,41 +1,25 @@
|
|||||||
À première vue, une brique LEGO reste une brique : un parallélépipède de plastique, discret et familier, qui évoque les chambres d’enfants et les châteaux improvisés sur le tapis du salon. Pourtant, depuis quelques années, certaines de ces briques ont acquis une tout autre nature. On les appelle **Smart Bricks**, et sous cette appellation presque anodine se cache une petite révolution silencieuse : celle de l’intelligence embarquée qui s’invite dans l’univers du jeu.
|
## La brique qui répond
|
||||||
|
|
||||||
La Smart Brick n’est pas seulement un élément de construction, elle est un cerveau miniature. Là où la brique traditionnelle se contente de tenir les autres ensemble, celle-ci observe, calcule, décide. Elle reçoit des informations venues de capteurs, interprète des instructions programmées par l’utilisateur, puis déclenche des actions : un moteur qui se met en marche, une lumière qui s’allume, un robot qui change de trajectoire. À travers elle, LEGO a transformé l’acte de construire en une véritable expérience de conception, où l’imagination se prolonge dans la logique et l’expérimentation.
|
À première vue c'est une brique LEGO comme une autre. Un parallélépipède de plastique gris, le format classique, deux par quatre tenons sur le dessus. On pourrait la prendre, l'emboîter dans un mur, et ne rien remarquer. Sauf que celle-là parle. Elle fait du bruit, elle clignote, elle sait si vous la secouez ou si vous la posez à plat. À l'intérieur, LEGO a réussi à caser un accéléromètre, un capteur de lumière, un capteur de couleur, un haut-parleur miniature et une puce sur mesure plus petite qu'un seul tenon. C'est la **LEGO Smart Brick**, et elle est arrivée en boutique le 1ᵉʳ mars 2026.
|
||||||
|
|
||||||
Cette mutation a ouvert la porte à une nouvelle façon de jouer. Les modèles ne sont plus figés dans une posture décorative : ils vivent, réagissent, apprennent presque. Un véhicule peut désormais éviter un obstacle, un bras mécanique saisir un objet, un automate suivre une ligne tracée au sol. L’enfant ne se contente plus d’assembler des pièces ; il devient ingénieur, programmeur, inventeur d’un monde en mouvement. Chaque erreur devient une hypothèse à tester, chaque réussite une découverte. Le jeu se transforme en terrain d’exploration scientifique, sans jamais perdre sa part de rêve.
|
Il faut tout de suite tordre le cou à un malentendu. La Smart Brick, ce n'est pas un Mindstorms. Ce n'est pas du LEGO éducatif, ce n'est pas une plateforme pour apprendre à coder, et on ne programme rien du tout avec. C'est un objet beaucoup plus simple dans son intention : faire en sorte qu'un set LEGO réagisse quand on joue avec. Vous prenez le X-Wing de Luke Skywalker, vous le faites basculer pour décoller, le brique embarquée détecte le mouvement et joue le bruit du moteur. Vous posez la minifigurine de Dark Vador à côté, la brique la reconnaît grâce à un Smart Tag (une petite tuile codée), et elle déclenche la respiration emblématique du Seigneur Sith. C'est tout. Mais c'est déjà beaucoup.
|
||||||
|
|
||||||
C’est sans doute là que réside la force la plus subtile de la Smart Brick : dans sa dimension éducative. Derrière l’apparente simplicité des interfaces de programmation, elle initie à la pensée algorithmique, à la rigueur de la logique, à la patience qu’exige toute mise au point technique. Sans discours théorique, sans tableaux noirs ni équations imposées, elle enseigne les bases de l’informatique et de la robotique comme on apprend une langue étrangère en voyageant : par immersion, par tâtonnement, par plaisir.
|
LEGO appelle cet écosystème **Smart Play**. Il repose sur trois éléments. La Smart Brick elle-même, qui est le cerveau et le haut-parleur. Les Smart Tags, des tuiles plates qu'on accroche aux constructions et qui disent à la brique ce qu'elle doit faire à cet endroit (« ici tu joues un bruit de tir laser », « ici tu fais le bruit du réacteur »). Et les Smart Minifigures, des figurines avec un identifiant intégré, que la brique détecte quand on les approche. Le tout communique en local, sans appli obligatoire, sans écran, via un système maison que LEGO a baptisé BrickNet. C'est important : le pari est explicitement de faire de la techno **invisible**, pas de coller un smartphone entre l'enfant et le jouet.
|
||||||
|
|
||||||
Au fil des années, cette brique intelligente a elle-même grandi. Des premiers modèles un peu rustiques aux hubs modernes capables de communiquer sans fil et d’exécuter des programmes complexes, elle a suivi l’évolution rapide des technologies numériques. Aujourd’hui, elle se rapproche davantage d’un véritable micro-ordinateur que d’un simple accessoire de jeu. Pourtant, son enveloppe reste la même, fidèle au format LEGO, comme pour rappeler que l’innovation la plus profonde peut parfois se glisser dans les formes les plus familières.
|
Côté pratique, la brique se recharge sans fil. Elle tient environ deux heures et demie en jeu actif, se met en veille au bout de trois minutes d'inactivité et se réveille quand on la secoue. Au-delà d'une dizaine d'heures de veille, il faut la remettre sur son chargeur. Une application gratuite, **LEGO SMART Assist**, sert à régler le volume, donner un nom à ses briques, gérer plusieurs appareils, et surtout mettre à jour le firmware — parce que oui, une brique LEGO peut maintenant recevoir des mises à jour logicielles. On y est.
|
||||||
|
|
||||||
La Smart Brick occupe ainsi une place singulière, à la frontière entre le jouet et l’outil scientifique. Dans une salle de classe, elle devient support pédagogique. Dans un atelier de passionnés, elle sert de laboratoire miniature. Dans une chambre d’enfant, elle allume peut-être la première étincelle d’une vocation future. Elle prouve surtout qu’il n’y a pas d’âge pour comprendre la technologie, et que la science peut naître d’un simple geste : emboîter deux briques, puis leur donner vie.
|
Pour le lancement, LEGO a choisi Star Wars, et l'offre est un peu plus subtile qu'il n'y paraît. **Huit sets** sortent le 1ᵉʳ mars, mais seulement **trois contiennent réellement une Smart Brick**. Ce sont les coffrets dits *All-In-One*, qui embarquent la brique, son chargeur, des tags et des figurines intelligentes :
|
||||||
|
|
||||||
Plus qu’une innovation technique, la Smart Brick est un symbole. Celui d’un monde où la créativité manuelle rencontre l’intelligence numérique, où le jeu devient apprentissage, et où une petite brique de plastique peut ouvrir, discrètement, la porte vers les grandes aventures de la science et de l’ingénierie.
|
- **75421 — Chasseur TIE de Dark Vador** : 69,99 €, le ticket d'entrée.
|
||||||
|
- **75423 — Le X-Wing rouge de Luke Skywalker** : 89,99 €.
|
||||||
|
- **75427 — Duel dans la salle du trône & A-Wing** : 159,99 €, le plus gros, avec **deux** Smart Bricks.
|
||||||
|
|
||||||
Voici **les informations connues à ce jour (janvier 2026)** sur **la disponibilité de la Smart Brick LEGO** et les modèles où elle est proposée :
|
Les cinq autres sets — Millennium Falcon, Mos Eisley Cantina, AT-ST Endor, hutte de Yoda, Landspeeder de Luke — sont étiquetés Smart Play mais ne contiennent **pas** de brique. Ils embarquent juste des tags et des figurines compatibles. Pour qu'ils s'animent, il faut posséder une brique achetée dans l'un des trois coffrets *All-In-One*, et la déplacer d'un set à l'autre. C'est un choix commercial qu'on peut critiquer : un parent ou un grand-parent qui voit *Smart Play* sur la boîte de la Mos Eisley Cantina à 79,99 € a de quoi être surpris en rentrant à la maison.
|
||||||
|
|
||||||
### 📅 Date de sortie
|
Géographiquement, le lancement est restreint. Six pays seulement à l'ouverture : États-Unis, Royaume-Uni, France, Allemagne, Pologne, Australie. Le reste du monde attendra.
|
||||||
|
|
||||||
La **Smart Brick LEGO** (élément électronique interactif intégré à une brique standard) **sera disponible à partir du **1ᵉʳ mars 2026**, avec ouverture des précommandes dès début janvier 2026. ([Toms Guide][1])
|
Pourquoi est-ce intéressant au-delà du cas Star Wars ? Parce que LEGO ne fait pas ça pour vendre trois sets. La marque parle de **plus de vingt brevets** déposés sur la techno, et de la « plus grande évolution du système LEGO depuis l'introduction de la minifigurine en 1978 ». Le ton est ambitieux, et il y a déjà des rumeurs de déclinaisons sur les gammes Pokémon et Animal Crossing. Si le pari réussit, on parle d'une plateforme qui peut s'étendre à toute la production LEGO sur dix ou vingt ans. Si elle échoue, ce sera la deuxième tentative ratée après les Mindstorms et la gamme Boost, dans la longue liste des essais LEGO pour marier l'électronique au plastique.
|
||||||
|
|
||||||
### 🎁 Modèles / ensembles où la Smart Brick est proposée au lancement
|
Le point qui me semble vraiment réussi, c'est la philosophie sans écran. Là où la plupart des jouets connectés exigent une tablette pour fonctionner, où l'enfant finit en pratique à regarder un iPad plutôt qu'à jouer avec l'objet physique, LEGO a fait le choix inverse : l'application existe mais elle est facultative, toute l'interaction se passe entre les mains et les briques. C'est moins spectaculaire dans une démo marketing, mais c'est probablement plus juste pour des gamins de huit ans.
|
||||||
|
|
||||||
Pour son lancement, la Smart Brick est intégrée dans **trois sets LEGO Star Wars de la nouvelle gamme *SMART Play***. Ces ensembles incluent également des *Smart Tags* et des *Smart Minifigures* pour exploiter les effets interactifs (sons, lumières, réactions physiques) : ([Toms Guide][1])
|
Reste à voir ce que ça donne en vrai, sur le tapis du salon, après six mois d'utilisation, quand la batterie sera moins fringante et que la nouveauté se sera émoussée. C'est toujours là que se joue la vraie partie pour ce genre de produit. Mais sur le papier, et c'est rare, LEGO a sorti quelque chose qui ne ressemble à rien d'autre.
|
||||||
|
|
||||||
* Un set autour du **TIE Fighter de Dark Vador** (offrant un Smart Brick et des effets interactifs). ([HOTH BRICKS][2])
|
|
||||||
* Le **X-Wing “Red Five” de Luke Skywalker**, avec plusieurs Smart Tags et figurines intelligentes. ([HOTH BRICKS][2])
|
|
||||||
* Un set plus complet **“Throne Room Duel & A-Wing”**, qui inclut **deux Smart Bricks** et plusieurs composants intelligents. ([HOTH BRICKS][2])
|
|
||||||
|
|
||||||
Ces boîtes sont commercialisées comme **ensembles “All-In-One” SMART Play** : elles contiennent tout le nécessaire (Smart Brick(s), chargeur sans fil inclus, Smart Tags, figurines) pour découvrir et utiliser la technologie. ([LEGO][3])
|
|
||||||
|
|
||||||
### 🛒 Disponibilité
|
|
||||||
|
|
||||||
* **Précommandes disponibles maintenant** auprès de LEGO et de revendeurs. ([Toms Guide][1])
|
|
||||||
* **Livraison/arrivée en boutiques : 1er mars 2026**. ([Frandroid][4])
|
|
||||||
|
|
||||||
Si tu souhaites, je peux te lister précisément les **références LEGO Smart Play disponibles** (numéros de set + prix indicatifs) pour comparer.
|
|
||||||
|
|
||||||
[1]: https://www.tomsguide.fr/lego-smart-play-quels-sont-les-sets-qui-vont-profiter-des-briques-intelligentes/?utm_source=chatgpt.com "Lego Smart Play : quels sont les sets qui vont profiter des ..."
|
|
||||||
[2]: https://www.hothbricks.com/sur-le-shop-lego-ou-chez-amazon-les-nouveautes-lego-star-wars-compatibles-smart-play-sont-en-precommande/?utm_source=chatgpt.com "Les nouveautés LEGO Star Wars compatibles SMART ..."
|
|
||||||
[3]: https://www.lego.com/fr-fr/smart-play/all-sets?utm_source=chatgpt.com "Jouets LEGO® SMART Play"
|
|
||||||
[4]: https://www.frandroid.com/produits-android/maison-connectee/2902811_lego-smart-brick-tout-savoir-sur-la-brique-connectee-qui-revolutionne-la-construction?utm_source=chatgpt.com "LEGO Smart Brick : tout savoir sur la brique connectée qui ..."
|
|
||||||
@@ -1,13 +1,152 @@
|
|||||||
{
|
{
|
||||||
"uuid": "c8fa250e-d8b5-453a-a06a-799d53c3b6d1",
|
"uuid": "c8fa250e-d8b5-453a-a06a-799d53c3b6d1",
|
||||||
"slug": "la-smart-brick-de-lego-quand-la-brique-devient-intelligente",
|
"slug": "la-smart-brick-de-lego-quand-la-brique-devient-intelligente",
|
||||||
"title": "La Smart Brick de LEGO : quand la brique devient intelligente",
|
"title": "LEGO : La brique qui répond",
|
||||||
"author": "cedric@abonnel.fr",
|
"author": "cedric@abonnel.fr",
|
||||||
"published": true,
|
"published": true,
|
||||||
"published_at": "2026-01-13 20:26:53",
|
"published_at": "2026-01-13 20:26",
|
||||||
"created_at": "2026-01-13 20:26:53",
|
"created_at": "2026-01-13 20:26:53",
|
||||||
"updated_at": "2026-01-13 20:26:53",
|
"updated_at": "2026-05-11 22:45:23",
|
||||||
"revisions": [],
|
"revisions": [
|
||||||
|
{
|
||||||
|
"n": 1,
|
||||||
|
"date": "2026-05-11 21:54:06",
|
||||||
|
"comment": "",
|
||||||
|
"title": "LEGO : La brique qui répond"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"n": 2,
|
||||||
|
"date": "2026-05-11 22:45:23",
|
||||||
|
"comment": "",
|
||||||
|
"title": "LEGO : La brique qui répond"
|
||||||
|
}
|
||||||
|
],
|
||||||
"cover": "cover.jpg",
|
"cover": "cover.jpg",
|
||||||
|
"files_meta": {
|
||||||
|
"cover.jpg": {
|
||||||
|
"author": "",
|
||||||
|
"source_url": "https://thegadgetflow.com/wp-content/uploads/2026/01/LEGO-Interactive-smart-brick-featured-image-1.jpg"
|
||||||
|
},
|
||||||
|
"_thumb_035b34dd014ac80f-635197.jpg": {
|
||||||
|
"author": "",
|
||||||
|
"source_url": ""
|
||||||
|
},
|
||||||
|
"_thumb_5913d9738c966833-30567.jpg": {
|
||||||
|
"author": "",
|
||||||
|
"source_url": ""
|
||||||
|
},
|
||||||
|
"_thumb_7d6ba93f6ce840e7-13566.webp": {
|
||||||
|
"author": "",
|
||||||
|
"source_url": ""
|
||||||
|
},
|
||||||
|
"_thumb_d5dde33b48cd2761-66620.jpg": {
|
||||||
|
"author": "",
|
||||||
|
"source_url": ""
|
||||||
|
},
|
||||||
|
"_thumb_e99b946f131256b8-49943.jpg": {
|
||||||
|
"author": "",
|
||||||
|
"source_url": ""
|
||||||
|
},
|
||||||
|
"_thumb_fb5a5b1ffa000d12-30780.png": {
|
||||||
|
"author": "",
|
||||||
|
"source_url": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"external_links": [
|
||||||
|
{
|
||||||
|
"url": "https://www.begeek.fr/lego-smart-brick-les-briques-de-la-marque-danoise-deviennent-interactives-426477",
|
||||||
|
"name": "LEGO Smart Brick : les briques de la marque danoise deviennent interactives",
|
||||||
|
"added_at": "2026-05-11 21:47:32",
|
||||||
|
"meta": {
|
||||||
|
"mime": "text/html",
|
||||||
|
"size": 36385,
|
||||||
|
"description": "Avec les Smart Bricks, LEGO transforme chaque construction en une expérience connectée grâce à des capteurs, haut-parleurs et LED intégrés.",
|
||||||
|
"og_image": "/file?uuid=c8fa250e-d8b5-453a-a06a-799d53c3b6d1&name=_thumb_5913d9738c966833-30567.jpg",
|
||||||
|
"site_name": "Begeek",
|
||||||
|
"og_type": "article",
|
||||||
|
"language": "fr_FR",
|
||||||
|
"date": "2026-01-07T13:00:51+00:00",
|
||||||
|
"canonical": "https://www.begeek.fr/lego-smart-brick-les-briques-de-la-marque-danoise-deviennent-interactives-426477"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://www.mac4ever.com/divers/193948-lego-devoile-une-etonnante-brique-intelligente-au-ces-2026",
|
||||||
|
"name": "LEGO dévoile une étonnante brique intelligente au CES 2026",
|
||||||
|
"added_at": "2026-05-11 21:47:50",
|
||||||
|
"meta": {
|
||||||
|
"mime": "text/html",
|
||||||
|
"size": 121071,
|
||||||
|
"description": "Pour sa première apparition au CES de Las Vegas, LEGO frappe fort avec Smart Play, une nouvelle plateforme qui intègre de l'électronique miniaturisée…",
|
||||||
|
"og_image": "/file?uuid=c8fa250e-d8b5-453a-a06a-799d53c3b6d1&name=_thumb_7d6ba93f6ce840e7-13566.webp",
|
||||||
|
"site_name": "Mac4Ever",
|
||||||
|
"og_type": "article",
|
||||||
|
"language": "fr_FR",
|
||||||
|
"date": "2026-01-06T09:36:39+01:00",
|
||||||
|
"canonical": "https://www.mac4ever.com/divers/193948-lego-devoile-une-etonnante-brique-intelligente-au-ces-2026"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://www.clubic.com/actualite-593794-lego-devoile-smart-play-sa-plus-grosse-innovation-depuis-toujours.html",
|
||||||
|
"name": "LEGO dévoile Smart Play, sa plus grosse innovation \"depuis... toujours\" !",
|
||||||
|
"added_at": "2026-05-11 21:48:05",
|
||||||
|
"meta": {
|
||||||
|
"mime": "text/html",
|
||||||
|
"size": 251642,
|
||||||
|
"description": "Dans les couloirs du CES de Las Vegas, la marque LEGO a tenu à dévoiler une toute nouvelle brique intelligente : Smart Brick. Une brique qui va se charger de réagir de manière intelligente (et autonome) à son environnement, sans la moindre application ou écran externe.",
|
||||||
|
"og_image": "/file?uuid=c8fa250e-d8b5-453a-a06a-799d53c3b6d1&name=_thumb_d5dde33b48cd2761-66620.jpg",
|
||||||
|
"site_name": "clubic.com",
|
||||||
|
"og_type": "article",
|
||||||
|
"language": "fr_FR",
|
||||||
|
"date": "2026-01-07T09:56:00+01:00",
|
||||||
|
"canonical": "https://www.clubic.com/actualite-593794-lego-devoile-smart-play-sa-plus-grosse-innovation-depuis-toujours.html"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://www.solutions-magazine.com/lego-smart-brick/",
|
||||||
|
"name": "LEGO lance sa « smart brick » - Solutions Magazine",
|
||||||
|
"added_at": "2026-05-11 21:48:23",
|
||||||
|
"meta": {
|
||||||
|
"mime": "text/html",
|
||||||
|
"size": 582486,
|
||||||
|
"description": "Au CES, Lego a dévoilé un ensemble de nouvelles technologies insérées dans ses futurs jouets. Notamment sa « smart brick »",
|
||||||
|
"og_image": "/file?uuid=c8fa250e-d8b5-453a-a06a-799d53c3b6d1&name=_thumb_e99b946f131256b8-49943.jpg",
|
||||||
|
"site_name": "Solutions Magazine",
|
||||||
|
"og_type": "article",
|
||||||
|
"language": "fr_FR",
|
||||||
|
"date": "2026-01-10T15:56:46+00:00",
|
||||||
|
"canonical": "https://www.solutions-magazine.com/lego-smart-brick/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://www.lesnumeriques.com/jouet/lego-smart-play-on-a-mis-nos-gros-doigts-sur-la-petite-brique-magique-et-c-est-bluffant-n249358.html",
|
||||||
|
"name": "Lego Smart Play : on a mis nos gros doigts sur la petite brique magique, et c'est bluffant ! - Les Numériques",
|
||||||
|
"added_at": "2026-05-11 21:48:38",
|
||||||
|
"meta": {
|
||||||
|
"mime": "text/html",
|
||||||
|
"size": 219016,
|
||||||
|
"description": "Présentée il y a quelques jours au CES en exclusivité, la brique Lego connectée Smart Brick s'est dévoilée un peu plus à Paris. Après quelques minutes de démonstration, avouons-le : nous regrettons de ne plus avoir 8 ans pour jouer avec sans scrupule.",
|
||||||
|
"keywords": "lego",
|
||||||
|
"og_image": "/file?uuid=c8fa250e-d8b5-453a-a06a-799d53c3b6d1&name=_thumb_035b34dd014ac80f-635197.jpg",
|
||||||
|
"og_type": "article",
|
||||||
|
"date": "2026-01-11T06:00:00Z",
|
||||||
|
"canonical": "https://www.lesnumeriques.com/jouet/lego-smart-play-on-a-mis-nos-gros-doigts-sur-la-petite-brique-magique-et-c-est-bluffant-n249358.html"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://www.lego.com/fr-fr/smart-play",
|
||||||
|
"name": "Système LEGO® SMART Play™ | Boutique LEGO® officielle FR",
|
||||||
|
"added_at": "2026-05-11 21:48:47",
|
||||||
|
"meta": {
|
||||||
|
"mime": "text/html",
|
||||||
|
"size": 1219325,
|
||||||
|
"description": "Voici le système LEGO® SMART Play™, conçu pour stimuler la créativité des enfants. Découvrez cette évolution du jeu LEGO® qui vous répond à l’infini ! Plus d’informations ici",
|
||||||
|
"canonical": "https://www.lego.com/fr-fr/smart-play",
|
||||||
|
"og_image": "/file?uuid=c8fa250e-d8b5-453a-a06a-799d53c3b6d1&name=_thumb_fb5a5b1ffa000d12-30780.png"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"seo_title": "",
|
||||||
|
"seo_description": "",
|
||||||
|
"og_image": "",
|
||||||
"category": "loisirs"
|
"category": "loisirs"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
## La brique qui répond
|
||||||
|
|
||||||
|
À première vue c'est une brique LEGO comme une autre. Un parallélépipède de plastique gris, le format classique, deux par quatre tenons sur le dessus. On pourrait la prendre, l'emboîter dans un mur, et ne rien remarquer. Sauf que celle-là parle. Elle fait du bruit, elle clignote, elle sait si vous la secouez ou si vous la posez à plat. À l'intérieur, LEGO a réussi à caser un accéléromètre, un capteur de lumière, un capteur de couleur, un haut-parleur miniature et une puce sur mesure plus petite qu'un seul tenon. C'est la **LEGO Smart Brick**, et elle est arrivée en boutique le 1ᵉʳ mars 2026.
|
||||||
|
|
||||||
|
Il faut tout de suite tordre le cou à un malentendu. La Smart Brick, ce n'est pas un Mindstorms. Ce n'est pas du LEGO éducatif, ce n'est pas une plateforme pour apprendre à coder, et on ne programme rien du tout avec. C'est un objet beaucoup plus simple dans son intention : faire en sorte qu'un set LEGO réagisse quand on joue avec. Vous prenez le X-Wing de Luke Skywalker, vous le faites basculer pour décoller, le brique embarquée détecte le mouvement et joue le bruit du moteur. Vous posez la minifigurine de Dark Vador à côté, la brique la reconnaît grâce à un Smart Tag (une petite tuile codée), et elle déclenche la respiration emblématique du Seigneur Sith. C'est tout. Mais c'est déjà beaucoup.
|
||||||
|
|
||||||
|
LEGO appelle cet écosystème **Smart Play**. Il repose sur trois éléments. La Smart Brick elle-même, qui est le cerveau et le haut-parleur. Les Smart Tags, des tuiles plates qu'on accroche aux constructions et qui disent à la brique ce qu'elle doit faire à cet endroit (« ici tu joues un bruit de tir laser », « ici tu fais le bruit du réacteur »). Et les Smart Minifigures, des figurines avec un identifiant intégré, que la brique détecte quand on les approche. Le tout communique en local, sans appli obligatoire, sans écran, via un système maison que LEGO a baptisé BrickNet. C'est important : le pari est explicitement de faire de la techno **invisible**, pas de coller un smartphone entre l'enfant et le jouet.
|
||||||
|
|
||||||
|
Côté pratique, la brique se recharge sans fil. Elle tient environ deux heures et demie en jeu actif, se met en veille au bout de trois minutes d'inactivité et se réveille quand on la secoue. Au-delà d'une dizaine d'heures de veille, il faut la remettre sur son chargeur. Une application gratuite, **LEGO SMART Assist**, sert à régler le volume, donner un nom à ses briques, gérer plusieurs appareils, et surtout mettre à jour le firmware — parce que oui, une brique LEGO peut maintenant recevoir des mises à jour logicielles. On y est.
|
||||||
|
|
||||||
|
Pour le lancement, LEGO a choisi Star Wars, et l'offre est un peu plus subtile qu'il n'y paraît. **Huit sets** sortent le 1ᵉʳ mars, mais seulement **trois contiennent réellement une Smart Brick**. Ce sont les coffrets dits *All-In-One*, qui embarquent la brique, son chargeur, des tags et des figurines intelligentes :
|
||||||
|
|
||||||
|
- **75421 — Chasseur TIE de Dark Vador** : 69,99 €, le ticket d'entrée.
|
||||||
|
- **75423 — Le X-Wing rouge de Luke Skywalker** : 89,99 €.
|
||||||
|
- **75427 — Duel dans la salle du trône & A-Wing** : 159,99 €, le plus gros, avec **deux** Smart Bricks.
|
||||||
|
|
||||||
|
Les cinq autres sets — Millennium Falcon, Mos Eisley Cantina, AT-ST Endor, hutte de Yoda, Landspeeder de Luke — sont étiquetés Smart Play mais ne contiennent **pas** de brique. Ils embarquent juste des tags et des figurines compatibles. Pour qu'ils s'animent, il faut posséder une brique achetée dans l'un des trois coffrets *All-In-One*, et la déplacer d'un set à l'autre. C'est un choix commercial qu'on peut critiquer : un parent ou un grand-parent qui voit *Smart Play* sur la boîte de la Mos Eisley Cantina à 79,99 € a de quoi être surpris en rentrant à la maison.
|
||||||
|
|
||||||
|
Géographiquement, le lancement est restreint. Six pays seulement à l'ouverture : États-Unis, Royaume-Uni, France, Allemagne, Pologne, Australie. Le reste du monde attendra.
|
||||||
|
|
||||||
|
Pourquoi est-ce intéressant au-delà du cas Star Wars ? Parce que LEGO ne fait pas ça pour vendre trois sets. La marque parle de **plus de vingt brevets** déposés sur la techno, et de la « plus grande évolution du système LEGO depuis l'introduction de la minifigurine en 1978 ». Le ton est ambitieux, et il y a déjà des rumeurs de déclinaisons sur les gammes Pokémon et Animal Crossing. Si le pari réussit, on parle d'une plateforme qui peut s'étendre à toute la production LEGO sur dix ou vingt ans. Si elle échoue, ce sera la deuxième tentative ratée après les Mindstorms et la gamme Boost, dans la longue liste des essais LEGO pour marier l'électronique au plastique.
|
||||||
|
|
||||||
|
Le point qui me semble vraiment réussi, c'est la philosophie sans écran. Là où la plupart des jouets connectés exigent une tablette pour fonctionner, où l'enfant finit en pratique à regarder un iPad plutôt qu'à jouer avec l'objet physique, LEGO a fait le choix inverse : l'application existe mais elle est facultative, toute l'interaction se passe entre les mains et les briques. C'est moins spectaculaire dans une démo marketing, mais c'est probablement plus juste pour des gamins de huit ans.
|
||||||
|
|
||||||
|
Reste à voir ce que ça donne en vrai, sur le tapis du salon, après six mois d'utilisation, quand la batterie sera moins fringante et que la nouveauté se sera émoussée. C'est toujours là que se joue la vraie partie pour ce genre de produit. Mais sur le papier, et c'est rare, LEGO a sorti quelque chose qui ne ressemble à rien d'autre.
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
## La brique qui répond
|
||||||
|
|
||||||
|
À première vue c'est une brique LEGO comme une autre. Un parallélépipède de plastique gris, le format classique, deux par quatre tenons sur le dessus. On pourrait la prendre, l'emboîter dans un mur, et ne rien remarquer. Sauf que celle-là parle. Elle fait du bruit, elle clignote, elle sait si vous la secouez ou si vous la posez à plat. À l'intérieur, LEGO a réussi à caser un accéléromètre, un capteur de lumière, un capteur de couleur, un haut-parleur miniature et une puce sur mesure plus petite qu'un seul tenon. C'est la **LEGO Smart Brick**, et elle est arrivée en boutique le 1ᵉʳ mars 2026.
|
||||||
|
|
||||||
|
Il faut tout de suite tordre le cou à un malentendu. La Smart Brick, ce n'est pas un Mindstorms. Ce n'est pas du LEGO éducatif, ce n'est pas une plateforme pour apprendre à coder, et on ne programme rien du tout avec. C'est un objet beaucoup plus simple dans son intention : faire en sorte qu'un set LEGO réagisse quand on joue avec. Vous prenez le X-Wing de Luke Skywalker, vous le faites basculer pour décoller, le brique embarquée détecte le mouvement et joue le bruit du moteur. Vous posez la minifigurine de Dark Vador à côté, la brique la reconnaît grâce à un Smart Tag (une petite tuile codée), et elle déclenche la respiration emblématique du Seigneur Sith. C'est tout. Mais c'est déjà beaucoup.
|
||||||
|
|
||||||
|
LEGO appelle cet écosystème **Smart Play**. Il repose sur trois éléments. La Smart Brick elle-même, qui est le cerveau et le haut-parleur. Les Smart Tags, des tuiles plates qu'on accroche aux constructions et qui disent à la brique ce qu'elle doit faire à cet endroit (« ici tu joues un bruit de tir laser », « ici tu fais le bruit du réacteur »). Et les Smart Minifigures, des figurines avec un identifiant intégré, que la brique détecte quand on les approche. Le tout communique en local, sans appli obligatoire, sans écran, via un système maison que LEGO a baptisé BrickNet. C'est important : le pari est explicitement de faire de la techno **invisible**, pas de coller un smartphone entre l'enfant et le jouet.
|
||||||
|
|
||||||
|
Côté pratique, la brique se recharge sans fil. Elle tient environ deux heures et demie en jeu actif, se met en veille au bout de trois minutes d'inactivité et se réveille quand on la secoue. Au-delà d'une dizaine d'heures de veille, il faut la remettre sur son chargeur. Une application gratuite, **LEGO SMART Assist**, sert à régler le volume, donner un nom à ses briques, gérer plusieurs appareils, et surtout mettre à jour le firmware — parce que oui, une brique LEGO peut maintenant recevoir des mises à jour logicielles. On y est.
|
||||||
|
|
||||||
|
Pour le lancement, LEGO a choisi Star Wars, et l'offre est un peu plus subtile qu'il n'y paraît. **Huit sets** sortent le 1ᵉʳ mars, mais seulement **trois contiennent réellement une Smart Brick**. Ce sont les coffrets dits *All-In-One*, qui embarquent la brique, son chargeur, des tags et des figurines intelligentes :
|
||||||
|
|
||||||
|
- **75421 — Chasseur TIE de Dark Vador** : 69,99 €, le ticket d'entrée.
|
||||||
|
- **75423 — Le X-Wing rouge de Luke Skywalker** : 89,99 €.
|
||||||
|
- **75427 — Duel dans la salle du trône & A-Wing** : 159,99 €, le plus gros, avec **deux** Smart Bricks.
|
||||||
|
|
||||||
|
Les cinq autres sets — Millennium Falcon, Mos Eisley Cantina, AT-ST Endor, hutte de Yoda, Landspeeder de Luke — sont étiquetés Smart Play mais ne contiennent **pas** de brique. Ils embarquent juste des tags et des figurines compatibles. Pour qu'ils s'animent, il faut posséder une brique achetée dans l'un des trois coffrets *All-In-One*, et la déplacer d'un set à l'autre. C'est un choix commercial qu'on peut critiquer : un parent ou un grand-parent qui voit *Smart Play* sur la boîte de la Mos Eisley Cantina à 79,99 € a de quoi être surpris en rentrant à la maison.
|
||||||
|
|
||||||
|
Géographiquement, le lancement est restreint. Six pays seulement à l'ouverture : États-Unis, Royaume-Uni, France, Allemagne, Pologne, Australie. Le reste du monde attendra.
|
||||||
|
|
||||||
|
Pourquoi est-ce intéressant au-delà du cas Star Wars ? Parce que LEGO ne fait pas ça pour vendre trois sets. La marque parle de **plus de vingt brevets** déposés sur la techno, et de la « plus grande évolution du système LEGO depuis l'introduction de la minifigurine en 1978 ». Le ton est ambitieux, et il y a déjà des rumeurs de déclinaisons sur les gammes Pokémon et Animal Crossing. Si le pari réussit, on parle d'une plateforme qui peut s'étendre à toute la production LEGO sur dix ou vingt ans. Si elle échoue, ce sera la deuxième tentative ratée après les Mindstorms et la gamme Boost, dans la longue liste des essais LEGO pour marier l'électronique au plastique.
|
||||||
|
|
||||||
|
Le point qui me semble vraiment réussi, c'est la philosophie sans écran. Là où la plupart des jouets connectés exigent une tablette pour fonctionner, où l'enfant finit en pratique à regarder un iPad plutôt qu'à jouer avec l'objet physique, LEGO a fait le choix inverse : l'application existe mais elle est facultative, toute l'interaction se passe entre les mains et les briques. C'est moins spectaculaire dans une démo marketing, mais c'est probablement plus juste pour des gamins de huit ans.
|
||||||
|
|
||||||
|
Reste à voir ce que ça donne en vrai, sur le tapis du salon, après six mois d'utilisation, quand la batterie sera moins fringante et que la nouveauté se sera émoussée. C'est toujours là que se joue la vraie partie pour ce genre de produit. Mais sur le papier, et c'est rare, LEGO a sorti quelque chose qui ne ressemble à rien d'autre.
|
||||||
|
After Width: | Height: | Size: 970 KiB |
|
After Width: | Height: | Size: 875 B |
|
After Width: | Height: | Size: 121 KiB |
|
After Width: | Height: | Size: 249 KiB |
|
After Width: | Height: | Size: 3.2 MiB |
|
After Width: | Height: | Size: 352 KiB |
|
After Width: | Height: | Size: 70 KiB |
@@ -1,78 +1,61 @@
|
|||||||
Alors que les discussions publiques se focalisent souvent sur la 5G et les futures générations mobiles, une révolution plus discrète mais tout aussi fondamentale est en cours : **la disparition progressive du cuivre** dans les réseaux fixes. Le cuivre, utilisé depuis des décennies pour le téléphone et l’ADSL, cède la place à la fibre optique, plus rapide, plus fiable et capable de supporter les usages ultra-connectés de demain.
|
La fibre optique a déjà remplacé le cuivre dans la plupart des déploiements neufs, et les opérateurs ont passé la dernière décennie à généraliser le GPON puis le XGS-PON. Mais la course aux débits ne s'arrête pas là. La prochaine marche s'appelle le **50G-PON**, et elle est en train de passer du statut de standard sur le papier à celui de technologie qu'on commence à voir en démonstration chez les équipementiers. Voilà ce qu'il faut en retenir.
|
||||||
|
|
||||||
Parmi les technologies qui matérialisent cette transition, le **50G-PON** (Passive Optical Network à 50 Gbit/s) représente le futur de la connectivité fixe. Destiné à remplacer le GPON (1 Gbit/s) et le XGS-PON (10 Gbit/s), il illustre la manière dont la fibre prend le relais du cuivre, tant pour les particuliers que pour les entreprises.
|
## Ce que c'est
|
||||||
|
|
||||||
---
|
Le 50G-PON est la dernière génération de réseau optique passif normalisée par l'ITU-T sous la référence **G.9804**. Comme ses prédécesseurs, il repose sur le principe d'une fibre unique partagée entre plusieurs abonnés via des splitters passifs — pas d'électronique active entre le central et le client. Ce qui change, c'est le débit : **50 Gbit/s symétriques** sur une seule longueur d'onde.
|
||||||
|
|
||||||
### Définition et principes
|
Pour situer la techno dans sa famille :
|
||||||
|
|
||||||
* **PON (Passive Optical Network)** : réseau optique passif partagé entre plusieurs abonnés.
|
- **GPON** : 2,5 Gbit/s descendant / 1,25 Gbit/s montant — la base du déploiement résidentiel actuel
|
||||||
* **50G-PON** : dernière génération standardisée par l’ITU-T (G.9804), offrant **débits symétriques jusqu’à 50 Gbit/s** par fibre partagée.
|
- **XGS-PON** : 10 Gbit/s symétriques — la génération qui prend le relais aujourd'hui
|
||||||
* **Évolution par rapport au cuivre :**
|
- **NG-PON2** : 40 Gbit/s, obtenus en agrégeant quatre canaux de 10 Gbit/s sur des longueurs d'onde différentes
|
||||||
|
- **50G-PON** : 50 Gbit/s symétriques sur une longueur d'onde unique
|
||||||
|
|
||||||
* **GPON** : 1 Gbit/s – déjà bien plus rapide que l’ADSL cuivre.
|
Le point intéressant, c'est précisément ce dernier détail. Là où NG-PON2 multipliait les canaux pour atteindre 40 Gbit/s — au prix d'une électronique plus complexe et plus chère — le 50G-PON tape les 50 Gbit/s sur **une seule porteuse**. C'est techniquement plus exigeant côté composants optiques, mais beaucoup plus simple à industrialiser et à exploiter.
|
||||||
* **XGS-PON** : 10 Gbit/s – multiplié par 10 par rapport au GPON.
|
|
||||||
* **NG-PON2** : 40 Gbit/s – agrégation de 4 × 10 Gbit/s.
|
|
||||||
* **50G-PON** : 50 Gbit/s symétriques – le cuivre ne peut tout simplement pas suivre.
|
|
||||||
|
|
||||||
---
|
## Comment ça marche
|
||||||
|
|
||||||
### Pourquoi la fibre remplace le cuivre
|
L'architecture reste celle du PON classique, ce qui est un choix volontaire pour garantir la coexistence avec les générations précédentes :
|
||||||
|
|
||||||
1. **Débits massifs** : les lignes cuivre plafonnent à quelques centaines de Mbit/s en VDSL2, tandis que la fibre monte facilement à 50 Gbit/s.
|
```
|
||||||
2. **Fiabilité** : la fibre est moins sensible aux perturbations électromagnétiques et aux pertes de signal sur de longues distances.
|
OLT (central) ──── fibre ──── [Splitter passif] ─┬─── ONT abonné 1
|
||||||
3. **Évolutivité** : la même infrastructure peut être mise à jour par simple changement d’équipements actifs, sans remplacer les câbles.
|
├─── ONT abonné 2
|
||||||
4. **Symétrie** : le cuivre ne peut offrir des débits montants équivalents aux débits descendants, contrairement à la fibre.
|
└─── ONT abonné 3
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Architecture simplifiée
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
flowchart LR
|
|
||||||
OLT[Optical Line Terminal - central] -->|Fibre optique unique| splitter[Splitters passifs]
|
|
||||||
splitter --> ONU1[Abonné 1]
|
|
||||||
splitter --> ONU2[Abonné 2]
|
|
||||||
splitter --> ONU3[Abonné 3]
|
|
||||||
```
|
```
|
||||||
|
|
||||||
* **OLT** : équipement central chez l’opérateur.
|
- L'**OLT** (*Optical Line Terminal*), côté opérateur, pilote le réseau et émet le signal.
|
||||||
* **Splitters passifs** : distribuent le signal à plusieurs abonnés sans amplification.
|
- Les **splitters passifs** dupliquent le signal lumineux pour le distribuer, sans alimentation ni amplification.
|
||||||
* **ONU/ONT** : terminaison optique côté abonné.
|
- L'**ONT** (*Optical Network Terminal*), chez l'abonné, fait la conversion optique-électrique.
|
||||||
|
|
||||||
Cette architecture est impossible à réaliser avec le cuivre à ces débits.
|
L'astuce du 50G-PON, c'est qu'il utilise **des longueurs d'onde différentes** de celles du GPON et du XGS-PON. Concrètement, les trois technologies peuvent **cohabiter sur la même fibre physique** : un opérateur peut continuer à servir ses abonnés GPON existants tout en branchant des nouveaux clients en XGS-PON ou en 50G-PON, sans retoucher l'infrastructure passive. C'est un point décisif pour le déploiement, parce qu'il évite la rupture de service et étale l'investissement.
|
||||||
|
|
||||||
---
|
## Pourquoi ça compte
|
||||||
|
|
||||||
### Limites et défis
|
À 50 Gbit/s symétriques, on n'est plus dans la logique du « plus de débit pour le particulier ». L'enjeu est ailleurs, et il est triple.
|
||||||
|
|
||||||
* **Coût de déploiement** : remplacer les infrastructures cuivre existantes par la fibre demande des investissements lourds.
|
D'abord, **les usages professionnels** qui tournent en limite sur XGS-PON. Sauvegarde cloud à l'échelle d'une entreprise, synchronisation inter-sites, stockage partagé, environnements de travail virtualisés : ces flux ont besoin de débit symétrique et constant, et 10 Gbit/s commencent à serrer dans certains contextes.
|
||||||
* **Adoption progressive** : tous les foyers ne nécessitent pas 50 Gbit/s, mais la fibre prépare l’avenir.
|
|
||||||
* **Interopérabilité** : certains équipements anciens ne peuvent pas être réutilisés.
|
|
||||||
* **Maintenance et consommation** : les équipements optiques haute vitesse demandent plus de puissance et de contrôle que le cuivre.
|
|
||||||
|
|
||||||
---
|
Ensuite, le **transport pour le mobile**. Une antenne 5G — et a fortiori 6G — doit être raccordée au cœur de réseau par un lien capable d'encaisser le trafic agrégé de tous les utilisateurs qu'elle sert. C'est ce qu'on appelle le *fronthaul* ou le *backhaul* selon l'architecture. Le 50G-PON est un candidat sérieux pour ce rôle, parce qu'il offre les bons débits avec une infrastructure mutualisable et peu coûteuse à exploiter.
|
||||||
|
|
||||||
### Usages envisagés
|
Enfin, **l'évolutivité**. La même fibre, le même splitter, le même chemin physique pourront porter le 50G-PON aujourd'hui et la génération suivante — déjà en discussion à l'ITU-T sous le nom de 100G-PON — demain. C'est ce qui justifie qu'on déploie du 50G-PON même si tous les abonnés n'en ont pas l'usage immédiat : ce n'est pas l'équipement client qui coûte cher, c'est la fibre dans la rue, et elle est déjà là.
|
||||||
|
|
||||||
* **Entreprises et datacenters** : backup cloud, stockage partagé, synchronisation massive.
|
## Ce qui freine encore
|
||||||
* **Télétravail et multimédia** : visioconférences 8K, cloud gaming, streaming multiple.
|
|
||||||
* **Futurs usages** : smart cities, IoT à grande échelle, et éventuellement fronthaul/backhaul pour la 6G fixe.
|
|
||||||
|
|
||||||
---
|
Le 50G-PON existe, il est standardisé, et plusieurs équipementiers proposent du matériel compatible. Pour autant, le déploiement à grande échelle prendra du temps, pour quelques raisons concrètes.
|
||||||
|
|
||||||
### Comparaison rapide
|
Le **coût des équipements** reste élevé. Les composants optiques capables de moduler proprement à 50 Gbit/s sur une seule porteuse sont à un stade industriel récent, et les volumes ne sont pas encore là pour faire baisser les prix. Pour la majorité des foyers, le XGS-PON couvre largement les besoins et coûte beaucoup moins cher.
|
||||||
|
|
||||||
| Technologie | Débit symétrique | Usages typiques |
|
La **consommation énergétique** est plus importante que sur les générations précédentes. Ce n'est pas rédhibitoire, mais ça compte dans le bilan d'exploitation, surtout à l'échelle d'un opérateur.
|
||||||
| ----------- | ---------------- | ----------------------------------- |
|
|
||||||
| Cuivre ADSL | 1–20 Mbit/s | Internet basique, téléphonie |
|
|
||||||
| VDSL2 | 50–100 Mbit/s | Internet résidentiel amélioré |
|
|
||||||
| GPON | 1 Gbit/s | Internet haut débit résidentiel |
|
|
||||||
| XGS-PON | 10 Gbit/s | PME, streaming HD, cloud |
|
|
||||||
| 50G-PON | 50 Gbit/s | Ultra-HD, datacenters, smart cities |
|
|
||||||
|
|
||||||
---
|
Enfin, **le marché n'est pas pressé**. Les box résidentielles actuelles n'exploiteraient même pas 10 Gbit/s symétriques, et les usages qui justifient le 50G-PON sont aujourd'hui concentrés sur des segments précis — entreprises, datacenters, opérateurs mobiles. Le déploiement va donc se faire par couches, en commençant par les zones où la demande existe vraiment.
|
||||||
|
|
||||||
La transition **cuivre → fibre** n’est pas qu’une question de vitesse : c’est un changement structurel. Le cuivre, malgré sa robustesse historique, atteint ses limites. La fibre, à travers des technologies comme le 50G-PON, est en train de devenir le **nouveau standard universel**, prêt à supporter les besoins numériques des prochaines décennies.
|
## En résumé
|
||||||
|
|
||||||
> En résumé, si le cuivre disparaît, ce n’est pas seulement pour la vitesse, mais pour **permettre l’infrastructure du futur**, capable de répondre aux besoins toujours croissants en connectivité.
|
| Technologie | Débit symétrique | Cible principale |
|
||||||
|
|---|---|---|
|
||||||
|
| GPON | 1 Gbit/s | Résidentiel actuel |
|
||||||
|
| XGS-PON | 10 Gbit/s | Résidentiel haut de gamme, PME |
|
||||||
|
| NG-PON2 | 40 Gbit/s (4 × 10) | Niche, peu déployé |
|
||||||
|
| 50G-PON | 50 Gbit/s | Entreprises, datacenters, transport mobile |
|
||||||
|
|
||||||
|
Le 50G-PON n'est pas la techno qui va arriver dans les box grand public dans les six mois. C'est la **brique d'infrastructure** qui prépare la décennie qui vient : celle qui permettra aux opérateurs de répondre à la fois aux besoins des entreprises, au raccordement des antennes mobiles de prochaine génération, et à la montée en puissance progressive du résidentiel — sans toucher à la fibre déjà tirée. Et c'est exactement ce qu'on attend d'une bonne infrastructure : qu'elle se mette en place sans bruit, et qu'elle dure.
|
||||||
|
|||||||
@@ -4,10 +4,147 @@
|
|||||||
"title": "50G-PON : la fibre optique du futur",
|
"title": "50G-PON : la fibre optique du futur",
|
||||||
"author": "cedric@abonnel.fr",
|
"author": "cedric@abonnel.fr",
|
||||||
"published": true,
|
"published": true,
|
||||||
"published_at": "2025-11-05 08:48:01",
|
"published_at": "2025-11-05 08:48",
|
||||||
"created_at": "2025-11-05 08:48:01",
|
"created_at": "2025-11-05 08:48:01",
|
||||||
"updated_at": "2025-11-05 08:48:01",
|
"updated_at": "2026-05-11 23:54:38",
|
||||||
"revisions": [],
|
"revisions": [
|
||||||
|
{
|
||||||
|
"n": 1,
|
||||||
|
"date": "2026-05-11 23:54:38",
|
||||||
|
"comment": "",
|
||||||
|
"title": "50G-PON : la fibre optique du futur"
|
||||||
|
}
|
||||||
|
],
|
||||||
"cover": "cover.jpg",
|
"cover": "cover.jpg",
|
||||||
|
"files_meta": {
|
||||||
|
"cover.jpg": {
|
||||||
|
"author": "",
|
||||||
|
"source_url": "https://www.zdnet.fr/wp-content/uploads/zdnet/2025/10/fibre-1125x615.jpg"
|
||||||
|
},
|
||||||
|
"_thumb_63b0e27c3950cfe1-993774.webp": {
|
||||||
|
"author": "",
|
||||||
|
"source_url": ""
|
||||||
|
},
|
||||||
|
"_thumb_6dea6d617384f5ed-875.gif": {
|
||||||
|
"author": "",
|
||||||
|
"source_url": ""
|
||||||
|
},
|
||||||
|
"_thumb_7df96bdf1ecebb75-124286.jpg": {
|
||||||
|
"author": "",
|
||||||
|
"source_url": ""
|
||||||
|
},
|
||||||
|
"_thumb_bfd46debd51361c4-255309.jpg": {
|
||||||
|
"author": "",
|
||||||
|
"source_url": ""
|
||||||
|
},
|
||||||
|
"_thumb_c436b74420666bbb-3393394.png": {
|
||||||
|
"author": "",
|
||||||
|
"source_url": ""
|
||||||
|
},
|
||||||
|
"_thumb_e58b3803bdfafdf0-360838.png": {
|
||||||
|
"author": "",
|
||||||
|
"source_url": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"external_links": [
|
||||||
|
{
|
||||||
|
"url": "https://hellofuture.orange.com/fr/50g-pon-orange-ouvre-la-voie-a-la-fibre-du-futur/",
|
||||||
|
"name": "50G-PON : Orange ouvre la voie à la Fibre du futur - Hello Future",
|
||||||
|
"added_at": "2026-05-11 23:52:53",
|
||||||
|
"meta": {
|
||||||
|
"mime": "text/html",
|
||||||
|
"size": 117887,
|
||||||
|
"description": "Sur la standardisation de la triple coexistence de trois technologies PON Depuis 2006, Orange déploie la fibre jusqu’aux domiciles (FTTH-Fibre To The Home) et jusqu’aux entreprises (FTTE-Fibre To The Entreprise). Les réseaux d’accès fibre d’Orange reposent sur des infrastructures passives (fibres, câbles, connecteurs, boitiers etc.) et des systèmes de transmissions…",
|
||||||
|
"og_image": "/file?uuid=e739bf3c-b380-4567-90aa-32da12f56bc5&name=_thumb_bfd46debd51361c4-255309.jpg",
|
||||||
|
"site_name": "Hello Future",
|
||||||
|
"og_type": "article",
|
||||||
|
"language": "fr_FR",
|
||||||
|
"date": "2024-06-25T10:27:25+01:00",
|
||||||
|
"canonical": "https://hellofuture.orange.com/fr/50g-pon-orange-ouvre-la-voie-a-la-fibre-du-futur/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://www.universfreebox.com/article/587477/orange-teste-la-fibre-du-futur-avec-la-50g-pon-a-40-gbit-s-une-premiere-en-france",
|
||||||
|
"name": "Orange teste la \"fibre du futur\" avec la 50G-PON à 40 Gbit/s, une première en France",
|
||||||
|
"added_at": "2026-05-11 23:53:22",
|
||||||
|
"author": "Lucas Musset",
|
||||||
|
"meta": {
|
||||||
|
"mime": "text/html",
|
||||||
|
"size": 118235,
|
||||||
|
"description": "Orange teste la technologie 50G-PON, une première en France pour la fibre très haut débit. Ce jeudi 23 octobre 2025, Orange a présenté à Lyon et Marseille une démonstration grandeur nature de la technologie 50G-PON, considérée comme la prochaine étape majeure dans l’évolution des réseaux fibre optique. Il s’agit, selon l’opérateur, d’une première en France. […]",
|
||||||
|
"og_image": "/file?uuid=e739bf3c-b380-4567-90aa-32da12f56bc5&name=_thumb_c436b74420666bbb-3393394.png",
|
||||||
|
"site_name": "Univers Freebox",
|
||||||
|
"og_type": "article",
|
||||||
|
"language": "fr_FR",
|
||||||
|
"date": "2025-10-23T16:00:44+00:00",
|
||||||
|
"canonical": "https://www.universfreebox.com/article/587477/orange-teste-la-fibre-du-futur-avec-la-50g-pon-a-40-gbit-s-une-premiere-en-france"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://www.generation-nt.com/actualites/orange-50g-pon-fibre-ftth-debit-2064811",
|
||||||
|
"name": "Fibre du futur : Orange teste du 50G-PON en conditions réelles",
|
||||||
|
"added_at": "2026-05-11 23:53:33",
|
||||||
|
"author": "Jérôme G.",
|
||||||
|
"meta": {
|
||||||
|
"mime": "text/html",
|
||||||
|
"size": 109809,
|
||||||
|
"description": "GNT est le portail Hi-Tech français consacré aux nouvelles technologies (internet, logiciel, matériel, mobilité, entreprise) et au jeu vidéo PC et consoles.",
|
||||||
|
"keywords": "50g-pon orange fibre optique , orange, 50g, pon, fibre, ftth, debit",
|
||||||
|
"og_image": "/file?uuid=e739bf3c-b380-4567-90aa-32da12f56bc5&name=_thumb_63b0e27c3950cfe1-993774.webp",
|
||||||
|
"site_name": "Génération NT",
|
||||||
|
"og_type": "article",
|
||||||
|
"language": "fr_FR",
|
||||||
|
"canonical": "https://www.generation-nt.com/actualites/orange-50g-pon-fibre-ftth-debit-2064811"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://www.zdnet.fr/actualites/orange-teste-la-fibre-optique-du-futur-jusqua-cinq-fois-plus-rapide-484095.htm",
|
||||||
|
"name": "Orange teste la fibre optique du futur, jusqu’à cinq fois plus ra ...",
|
||||||
|
"added_at": "2026-05-11 23:54:09",
|
||||||
|
"author": "Xavier Biseul",
|
||||||
|
"meta": {
|
||||||
|
"mime": "text/html",
|
||||||
|
"size": 236667,
|
||||||
|
"description": "Dans ses laboratoires, l’opérateur historique mène des expérimentations sur la technologie 50G-PON. Ce nouveau standard des réseaux de fibre optique permet d’atteindre des débits théoriques allant jusqu’à 50 gigabits par seconde.",
|
||||||
|
"og_image": "/file?uuid=e739bf3c-b380-4567-90aa-32da12f56bc5&name=_thumb_7df96bdf1ecebb75-124286.jpg",
|
||||||
|
"site_name": "ZDNET",
|
||||||
|
"og_type": "article",
|
||||||
|
"language": "fr_FR",
|
||||||
|
"date": "2025-10-28T15:20:02+00:00",
|
||||||
|
"canonical": "https://www.zdnet.fr/actualites/orange-teste-la-fibre-optique-du-futur-jusqua-cinq-fois-plus-rapide-484095.htm"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://www.lyon-entreprises.com/actualites/article/orange-teste-a-lyon-la-fibre-du-futur-grace-a-la-technologie-50g-pon",
|
||||||
|
"name": "Orange teste à Lyon la fibre du futur grâce à la technologie 50G-PON - Lyon Entreprises [LE]",
|
||||||
|
"added_at": "2026-05-11 23:54:21",
|
||||||
|
"meta": {
|
||||||
|
"mime": "text/html",
|
||||||
|
"size": 161762,
|
||||||
|
"description": "Un article d'actualité LE [ Lyon-Entreprises ], le portail d'information sur les entreprises pour les décideurs de Lyon et Rhône-Alpes",
|
||||||
|
"og_image": "/file?uuid=e739bf3c-b380-4567-90aa-32da12f56bc5&name=_thumb_e58b3803bdfafdf0-360838.png",
|
||||||
|
"site_name": "Lyon Entreprises [LE]",
|
||||||
|
"og_type": "article",
|
||||||
|
"language": "fr_FR",
|
||||||
|
"canonical": "https://www.lyon-entreprises.com/actualites/article/orange-teste-a-lyon-la-fibre-du-futur-grace-a-la-technologie-50g-pon"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://lafibre.info/orange-les-news/50g-pon-orange-ouvre-la-voie-a-la-fibre-du-futur/",
|
||||||
|
"name": "50G-PON : Orange ouvre la voie à la Fibre du futur",
|
||||||
|
"added_at": "2026-05-11 23:54:30",
|
||||||
|
"meta": {
|
||||||
|
"mime": "text/html",
|
||||||
|
"size": 48608,
|
||||||
|
"description": "50G-PON : Orange ouvre la voie à la Fibre du futur",
|
||||||
|
"keywords": "Fibre optique,FTTH,très haut débit,Gpon,FTTLA,FTTdp,Test débit,SpeedTest",
|
||||||
|
"canonical": "https://lafibre.info/orange-les-news/50g-pon-orange-ouvre-la-voie-a-la-fibre-du-futur/",
|
||||||
|
"og_image": "/file?uuid=e739bf3c-b380-4567-90aa-32da12f56bc5&name=_thumb_6dea6d617384f5ed-875.gif"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"seo_title": "",
|
||||||
|
"seo_description": "",
|
||||||
|
"og_image": "",
|
||||||
"category": "télécom"
|
"category": "télécom"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
La fibre optique a déjà remplacé le cuivre dans la plupart des déploiements neufs, et les opérateurs ont passé la dernière décennie à généraliser le GPON puis le XGS-PON. Mais la course aux débits ne s'arrête pas là. La prochaine marche s'appelle le **50G-PON**, et elle est en train de passer du statut de standard sur le papier à celui de technologie qu'on commence à voir en démonstration chez les équipementiers. Voilà ce qu'il faut en retenir.
|
||||||
|
|
||||||
|
## Ce que c'est
|
||||||
|
|
||||||
|
Le 50G-PON est la dernière génération de réseau optique passif normalisée par l'ITU-T sous la référence **G.9804**. Comme ses prédécesseurs, il repose sur le principe d'une fibre unique partagée entre plusieurs abonnés via des splitters passifs — pas d'électronique active entre le central et le client. Ce qui change, c'est le débit : **50 Gbit/s symétriques** sur une seule longueur d'onde.
|
||||||
|
|
||||||
|
Pour situer la techno dans sa famille :
|
||||||
|
|
||||||
|
- **GPON** : 2,5 Gbit/s descendant / 1,25 Gbit/s montant — la base du déploiement résidentiel actuel
|
||||||
|
- **XGS-PON** : 10 Gbit/s symétriques — la génération qui prend le relais aujourd'hui
|
||||||
|
- **NG-PON2** : 40 Gbit/s, obtenus en agrégeant quatre canaux de 10 Gbit/s sur des longueurs d'onde différentes
|
||||||
|
- **50G-PON** : 50 Gbit/s symétriques sur une longueur d'onde unique
|
||||||
|
|
||||||
|
Le point intéressant, c'est précisément ce dernier détail. Là où NG-PON2 multipliait les canaux pour atteindre 40 Gbit/s — au prix d'une électronique plus complexe et plus chère — le 50G-PON tape les 50 Gbit/s sur **une seule porteuse**. C'est techniquement plus exigeant côté composants optiques, mais beaucoup plus simple à industrialiser et à exploiter.
|
||||||
|
|
||||||
|
## Comment ça marche
|
||||||
|
|
||||||
|
L'architecture reste celle du PON classique, ce qui est un choix volontaire pour garantir la coexistence avec les générations précédentes :
|
||||||
|
|
||||||
|
```
|
||||||
|
OLT (central) ──── fibre ──── [Splitter passif] ─┬─── ONT abonné 1
|
||||||
|
├─── ONT abonné 2
|
||||||
|
└─── ONT abonné 3
|
||||||
|
```
|
||||||
|
|
||||||
|
- L'**OLT** (*Optical Line Terminal*), côté opérateur, pilote le réseau et émet le signal.
|
||||||
|
- Les **splitters passifs** dupliquent le signal lumineux pour le distribuer, sans alimentation ni amplification.
|
||||||
|
- L'**ONT** (*Optical Network Terminal*), chez l'abonné, fait la conversion optique-électrique.
|
||||||
|
|
||||||
|
L'astuce du 50G-PON, c'est qu'il utilise **des longueurs d'onde différentes** de celles du GPON et du XGS-PON. Concrètement, les trois technologies peuvent **cohabiter sur la même fibre physique** : un opérateur peut continuer à servir ses abonnés GPON existants tout en branchant des nouveaux clients en XGS-PON ou en 50G-PON, sans retoucher l'infrastructure passive. C'est un point décisif pour le déploiement, parce qu'il évite la rupture de service et étale l'investissement.
|
||||||
|
|
||||||
|
## Pourquoi ça compte
|
||||||
|
|
||||||
|
À 50 Gbit/s symétriques, on n'est plus dans la logique du « plus de débit pour le particulier ». L'enjeu est ailleurs, et il est triple.
|
||||||
|
|
||||||
|
D'abord, **les usages professionnels** qui tournent en limite sur XGS-PON. Sauvegarde cloud à l'échelle d'une entreprise, synchronisation inter-sites, stockage partagé, environnements de travail virtualisés : ces flux ont besoin de débit symétrique et constant, et 10 Gbit/s commencent à serrer dans certains contextes.
|
||||||
|
|
||||||
|
Ensuite, le **transport pour le mobile**. Une antenne 5G — et a fortiori 6G — doit être raccordée au cœur de réseau par un lien capable d'encaisser le trafic agrégé de tous les utilisateurs qu'elle sert. C'est ce qu'on appelle le *fronthaul* ou le *backhaul* selon l'architecture. Le 50G-PON est un candidat sérieux pour ce rôle, parce qu'il offre les bons débits avec une infrastructure mutualisable et peu coûteuse à exploiter.
|
||||||
|
|
||||||
|
Enfin, **l'évolutivité**. La même fibre, le même splitter, le même chemin physique pourront porter le 50G-PON aujourd'hui et la génération suivante — déjà en discussion à l'ITU-T sous le nom de 100G-PON — demain. C'est ce qui justifie qu'on déploie du 50G-PON même si tous les abonnés n'en ont pas l'usage immédiat : ce n'est pas l'équipement client qui coûte cher, c'est la fibre dans la rue, et elle est déjà là.
|
||||||
|
|
||||||
|
## Ce qui freine encore
|
||||||
|
|
||||||
|
Le 50G-PON existe, il est standardisé, et plusieurs équipementiers proposent du matériel compatible. Pour autant, le déploiement à grande échelle prendra du temps, pour quelques raisons concrètes.
|
||||||
|
|
||||||
|
Le **coût des équipements** reste élevé. Les composants optiques capables de moduler proprement à 50 Gbit/s sur une seule porteuse sont à un stade industriel récent, et les volumes ne sont pas encore là pour faire baisser les prix. Pour la majorité des foyers, le XGS-PON couvre largement les besoins et coûte beaucoup moins cher.
|
||||||
|
|
||||||
|
La **consommation énergétique** est plus importante que sur les générations précédentes. Ce n'est pas rédhibitoire, mais ça compte dans le bilan d'exploitation, surtout à l'échelle d'un opérateur.
|
||||||
|
|
||||||
|
Enfin, **le marché n'est pas pressé**. Les box résidentielles actuelles n'exploiteraient même pas 10 Gbit/s symétriques, et les usages qui justifient le 50G-PON sont aujourd'hui concentrés sur des segments précis — entreprises, datacenters, opérateurs mobiles. Le déploiement va donc se faire par couches, en commençant par les zones où la demande existe vraiment.
|
||||||
|
|
||||||
|
## En résumé
|
||||||
|
|
||||||
|
| Technologie | Débit symétrique | Cible principale |
|
||||||
|
|---|---|---|
|
||||||
|
| GPON | 1 Gbit/s | Résidentiel actuel |
|
||||||
|
| XGS-PON | 10 Gbit/s | Résidentiel haut de gamme, PME |
|
||||||
|
| NG-PON2 | 40 Gbit/s (4 × 10) | Niche, peu déployé |
|
||||||
|
| 50G-PON | 50 Gbit/s | Entreprises, datacenters, transport mobile |
|
||||||
|
|
||||||
|
Le 50G-PON n'est pas la techno qui va arriver dans les box grand public dans les six mois. C'est la **brique d'infrastructure** qui prépare la décennie qui vient : celle qui permettra aux opérateurs de répondre à la fois aux besoins des entreprises, au raccordement des antennes mobiles de prochaine génération, et à la montée en puissance progressive du résidentiel — sans toucher à la fibre déjà tirée. Et c'est exactement ce qu'on attend d'une bonne infrastructure : qu'elle se mette en place sans bruit, et qu'elle dure.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
["scolaire"]
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
-- Migration 001 : Système de rôles et notes d'articles
|
||||||
|
-- À exécuter une seule fois sur le serveur PostgreSQL
|
||||||
|
|
||||||
|
-- Rôles disponibles
|
||||||
|
CREATE TABLE IF NOT EXISTS roles (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
name VARCHAR(50) UNIQUE NOT NULL,
|
||||||
|
label TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO roles (name, label) VALUES
|
||||||
|
('admin', 'Administrateur'),
|
||||||
|
('editor', 'Rédacteur'),
|
||||||
|
('reader', 'Lecteur')
|
||||||
|
ON CONFLICT (name) DO NOTHING;
|
||||||
|
|
||||||
|
-- Association utilisateur ↔ rôle (clé : email, pour compatibilité OIDC sans FK)
|
||||||
|
CREATE TABLE IF NOT EXISTS user_roles (
|
||||||
|
user_email TEXT NOT NULL,
|
||||||
|
role_id INTEGER NOT NULL REFERENCES roles(id) ON DELETE CASCADE,
|
||||||
|
granted_at TIMESTAMP DEFAULT NOW(),
|
||||||
|
granted_by TEXT,
|
||||||
|
PRIMARY KEY (user_email, role_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Seed : cedric@abonnel.fr → admin
|
||||||
|
INSERT INTO user_roles (user_email, role_id, granted_by)
|
||||||
|
SELECT 'cedric@abonnel.fr', id, 'migration'
|
||||||
|
FROM roles WHERE name = 'admin'
|
||||||
|
ON CONFLICT DO NOTHING;
|
||||||
|
|
||||||
|
-- Notes d'articles (1-5 étoiles, une note par utilisateur par article)
|
||||||
|
CREATE TABLE IF NOT EXISTS article_ratings (
|
||||||
|
article_uuid VARCHAR(36) NOT NULL,
|
||||||
|
user_email TEXT NOT NULL,
|
||||||
|
rating SMALLINT NOT NULL CHECK (rating BETWEEN 1 AND 5),
|
||||||
|
rated_at TIMESTAMP DEFAULT NOW(),
|
||||||
|
PRIMARY KEY (article_uuid, user_email)
|
||||||
|
);
|
||||||
@@ -115,13 +115,35 @@ body {
|
|||||||
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%2830%2C41%2C59%2C0.75%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e");
|
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%2830%2C41%2C59%2C0.75%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ─── Category nav in navbar ─────────────── */
|
||||||
|
.navbar-cats {
|
||||||
|
gap: 0.15rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-cat {
|
||||||
|
font-size: 0.8rem !important;
|
||||||
|
padding: 0.25rem 0.6rem !important;
|
||||||
|
border-radius: 2rem !important;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-cat.active {
|
||||||
|
background-color: var(--vl-accent) !important;
|
||||||
|
color: #fff !important;
|
||||||
|
}
|
||||||
|
|
||||||
/* ─── Main ───────────────────────────────── */
|
/* ─── Main ───────────────────────────────── */
|
||||||
main.container {
|
main.container,
|
||||||
max-width: 980px;
|
main.container-xl,
|
||||||
|
main.container-fluid {
|
||||||
padding-top: 2rem;
|
padding-top: 2rem;
|
||||||
padding-bottom: 3rem;
|
padding-bottom: 3rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
main.container {
|
||||||
|
max-width: 980px;
|
||||||
|
}
|
||||||
|
|
||||||
/* ─── Headings ───────────────────────────── */
|
/* ─── Headings ───────────────────────────── */
|
||||||
h1 {
|
h1 {
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
@@ -170,6 +192,139 @@ a:hover {
|
|||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.article-cover--gradient {
|
||||||
|
height: 160px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hero : titre sur l'image */
|
||||||
|
.article-cover--hero {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-cover--hero img {
|
||||||
|
height: clamp(260px, 45vw, 480px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-cover--hero.article-cover--gradient {
|
||||||
|
height: clamp(200px, 35vw, 340px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-hero-text {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0.9rem 1.4rem 1.25rem;
|
||||||
|
background: linear-gradient(
|
||||||
|
to top,
|
||||||
|
rgba(0,0,0,.80) 0%,
|
||||||
|
rgba(0,0,0,.32) 50%,
|
||||||
|
transparent 100%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-hero-top {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 0.4rem;
|
||||||
|
align-items: flex-start;
|
||||||
|
min-height: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-hero-bottom {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-hero-left { flex: 1; min-width: 0; }
|
||||||
|
|
||||||
|
.article-hero-right {
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-hero-text .cover-category {
|
||||||
|
position: static;
|
||||||
|
align-self: flex-start;
|
||||||
|
margin-bottom: 0.55rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-hero-text .article-title {
|
||||||
|
color: #fff;
|
||||||
|
text-shadow: 0 1px 8px rgba(0,0,0,.5);
|
||||||
|
margin-bottom: 0.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-hero-meta {
|
||||||
|
font-size: 0.82rem;
|
||||||
|
color: rgba(255,255,255,.72);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Boutons glass sur le hero */
|
||||||
|
.hero-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.25rem;
|
||||||
|
padding: 0.28rem 0.75rem;
|
||||||
|
font-size: 0.73rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #fff !important;
|
||||||
|
background: rgba(0,0,0,.35);
|
||||||
|
border: 1px solid rgba(255,255,255,.22);
|
||||||
|
border-radius: 999px;
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
-webkit-backdrop-filter: blur(8px);
|
||||||
|
text-decoration: none !important;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.15s;
|
||||||
|
white-space: nowrap;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
.hero-btn:hover { background: rgba(0,0,0,.58); color: #fff !important; }
|
||||||
|
.hero-btn--danger { border-color: rgba(239,68,68,.5); }
|
||||||
|
.hero-btn--danger:hover { background: rgba(180,30,30,.65); }
|
||||||
|
|
||||||
|
/* Score de notation dans le hero */
|
||||||
|
.hero-rating-score {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: rgba(255,255,255,.88);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Étoiles sur fond sombre */
|
||||||
|
.star-rating--hero label { color: rgba(255,255,255,.4); font-size: 1.1rem; }
|
||||||
|
.star-rating--hero input:checked ~ label,
|
||||||
|
.star-rating--hero label:hover,
|
||||||
|
.star-rating--hero label:hover ~ label { color: #f5c842; }
|
||||||
|
|
||||||
|
.card-cover {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover-category {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0.6rem;
|
||||||
|
left: 0.75rem;
|
||||||
|
font-size: 0.65rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: #fff;
|
||||||
|
background: rgba(0, 0, 0, 0.28);
|
||||||
|
padding: 0.2rem 0.55rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
backdrop-filter: blur(6px);
|
||||||
|
-webkit-backdrop-filter: blur(6px);
|
||||||
|
}
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
border: 1px solid var(--vl-border) !important;
|
border: 1px solid var(--vl-border) !important;
|
||||||
border-radius: var(--vl-radius) !important;
|
border-radius: var(--vl-radius) !important;
|
||||||
@@ -195,6 +350,15 @@ a:hover {
|
|||||||
color: var(--vl-text);
|
color: var(--vl-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.article-title {
|
||||||
|
font-weight: 800;
|
||||||
|
font-size: clamp(1.6rem, 4vw, 2.4rem);
|
||||||
|
line-height: 1.2;
|
||||||
|
letter-spacing: -0.5px;
|
||||||
|
color: var(--vl-text);
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
/* Override la couleur text-primary Bootstrap sur les card-title */
|
/* Override la couleur text-primary Bootstrap sur les card-title */
|
||||||
.card-title.text-primary {
|
.card-title.text-primary {
|
||||||
color: var(--vl-text) !important;
|
color: var(--vl-text) !important;
|
||||||
@@ -226,7 +390,8 @@ a:hover {
|
|||||||
|
|
||||||
/* ─── Ribbons (brouillon / avant-première) ── */
|
/* ─── Ribbons (brouillon / avant-première) ── */
|
||||||
.draft-ribbon,
|
.draft-ribbon,
|
||||||
.premiere-ribbon {
|
.premiere-ribbon,
|
||||||
|
.private-ribbon {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 26px;
|
top: 26px;
|
||||||
right: -34px;
|
right: -34px;
|
||||||
@@ -244,8 +409,9 @@ a:hover {
|
|||||||
box-shadow: 0 1px 3px rgba(0,0,0,.18);
|
box-shadow: 0 1px 3px rgba(0,0,0,.18);
|
||||||
}
|
}
|
||||||
|
|
||||||
.draft-ribbon { background: #f59e0b; }
|
.draft-ribbon { background: #f59e0b; }
|
||||||
.premiere-ribbon { background: #6366f1; }
|
.premiere-ribbon { background: #6366f1; }
|
||||||
|
.private-ribbon { background: #64748b; }
|
||||||
|
|
||||||
/* ─── Buttons ─────────────────────────────── */
|
/* ─── Buttons ─────────────────────────────── */
|
||||||
.btn {
|
.btn {
|
||||||
@@ -349,6 +515,195 @@ textarea.form-control {
|
|||||||
color: #991b1b;
|
color: #991b1b;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ─── Left category sidebar ──────────────── */
|
||||||
|
.left-sidebar {
|
||||||
|
position: sticky;
|
||||||
|
top: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-sidebar-section {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-sidebar-cat {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: .08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--vl-muted);
|
||||||
|
text-decoration: none;
|
||||||
|
margin-bottom: 0.4rem;
|
||||||
|
padding: 0.2rem 0.5rem;
|
||||||
|
border-radius: 0.4rem;
|
||||||
|
transition: background 0.15s, color 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-sidebar-cat:hover {
|
||||||
|
background: var(--vl-accent-soft);
|
||||||
|
color: var(--vl-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-sidebar-list {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-sidebar-list li {
|
||||||
|
border-left: 2px solid var(--vl-border);
|
||||||
|
padding-left: 0.6rem;
|
||||||
|
margin-bottom: 0.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-sidebar-list li a {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
line-height: 1.35;
|
||||||
|
color: var(--vl-text);
|
||||||
|
text-decoration: none;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: color 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-sidebar-list li a:hover {
|
||||||
|
color: var(--vl-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── Related articles sidebar ───────────── */
|
||||||
|
.related-sidebar {
|
||||||
|
position: sticky;
|
||||||
|
top: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── Post layout: colonnes sidebar fixe + article flexible ── */
|
||||||
|
@media (min-width: 992px) {
|
||||||
|
.post-sidebar-col {
|
||||||
|
flex: 0 0 260px;
|
||||||
|
width: 260px;
|
||||||
|
max-width: 260px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-sidebar-title {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: .08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--vl-muted);
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-card {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.75rem;
|
||||||
|
align-items: flex-start;
|
||||||
|
text-decoration: none;
|
||||||
|
color: var(--vl-text);
|
||||||
|
padding: 0.65rem;
|
||||||
|
border-radius: var(--vl-radius);
|
||||||
|
transition: background 0.15s;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-card:hover {
|
||||||
|
background: var(--vl-accent-soft);
|
||||||
|
color: var(--vl-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-card-thumb {
|
||||||
|
width: 64px;
|
||||||
|
height: 52px;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-card-body {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-card-title {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.35;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-card-date {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--vl-muted);
|
||||||
|
margin-top: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── Source / attachment cards (sidebar) ── */
|
||||||
|
.source-card {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.65rem;
|
||||||
|
align-items: flex-start;
|
||||||
|
text-decoration: none;
|
||||||
|
color: var(--vl-text);
|
||||||
|
padding: 0.5rem 0.65rem;
|
||||||
|
border-radius: var(--vl-radius);
|
||||||
|
border: 1px solid var(--vl-border);
|
||||||
|
background: var(--vl-surface);
|
||||||
|
transition: border-color 0.15s, box-shadow 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.source-card:hover {
|
||||||
|
border-color: var(--vl-accent);
|
||||||
|
box-shadow: 0 0 0 3px rgba(79,70,229,.08);
|
||||||
|
color: var(--vl-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.source-card-thumb {
|
||||||
|
width: 52px;
|
||||||
|
height: 42px;
|
||||||
|
border-radius: 0.4rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
background-color: var(--vl-accent-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.source-card-thumb--pdf,
|
||||||
|
.source-card-thumb--link {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
color: var(--vl-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.source-card-body {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.source-card-title {
|
||||||
|
font-size: 0.82rem;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.3;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.source-card-meta {
|
||||||
|
font-size: 0.72rem;
|
||||||
|
color: var(--vl-muted);
|
||||||
|
margin-top: 0.2rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
/* ─── Post content (Markdown rendu) ──────── */
|
/* ─── Post content (Markdown rendu) ──────── */
|
||||||
.post-content {
|
.post-content {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
@@ -554,6 +909,56 @@ footer {
|
|||||||
opacity: 0.75;
|
opacity: 0.75;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ─── Pagination ─────────────────────────── */
|
||||||
|
.pagination-nav {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.3rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-width: 2.2rem;
|
||||||
|
height: 2.2rem;
|
||||||
|
padding: 0 0.5rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
border: 1px solid var(--vl-border);
|
||||||
|
background: var(--vl-surface);
|
||||||
|
color: var(--vl-text);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: border-color 0.15s, background 0.15s, color 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-btn:hover:not(.disabled):not(.active) {
|
||||||
|
border-color: var(--vl-accent);
|
||||||
|
color: var(--vl-accent);
|
||||||
|
background: var(--vl-accent-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-btn.active {
|
||||||
|
background: var(--vl-accent);
|
||||||
|
border-color: var(--vl-accent);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-btn.disabled {
|
||||||
|
opacity: 0.35;
|
||||||
|
cursor: default;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-ellipsis {
|
||||||
|
padding: 0 0.25rem;
|
||||||
|
color: var(--vl-muted);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
/* ─── Footer 2 colonnes ──────────────────── */
|
/* ─── Footer 2 colonnes ──────────────────── */
|
||||||
footer {
|
footer {
|
||||||
border-top: 1px solid var(--vl-border);
|
border-top: 1px solid var(--vl-border);
|
||||||
@@ -639,3 +1044,27 @@ footer {
|
|||||||
.text-primary {
|
.text-primary {
|
||||||
color: var(--vl-accent) !important;
|
color: var(--vl-accent) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ─── Widget étoiles ──────────────────────── */
|
||||||
|
.star-rating {
|
||||||
|
display: inline-flex;
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.star-rating input {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.star-rating label {
|
||||||
|
font-size: 1.4rem;
|
||||||
|
color: #ccc;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color 0.1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.star-rating input:checked ~ label,
|
||||||
|
.star-rating label:hover,
|
||||||
|
.star-rating label:hover ~ label {
|
||||||
|
color: #f5a623;
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
|
||||||
// Auto-resize textarea au chargement et à la saisie
|
// ─── Auto-resize textareas ───────────────────────────────────────────────
|
||||||
document.querySelectorAll('textarea.form-control').forEach(function (ta) {
|
document.querySelectorAll('textarea.form-control').forEach(function (ta) {
|
||||||
function resize() {
|
function resize() {
|
||||||
ta.style.height = 'auto';
|
ta.style.height = 'auto';
|
||||||
@@ -12,7 +12,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
resize();
|
resize();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Ctrl+Enter (ou Cmd+Enter) pour soumettre le formulaire de post
|
// ─── Ctrl+Enter : soumettre le formulaire ────────────────────────────────
|
||||||
var form = document.querySelector('form[method="POST"]');
|
var form = document.querySelector('form[method="POST"]');
|
||||||
if (form) {
|
if (form) {
|
||||||
form.addEventListener('keydown', function (e) {
|
form.addEventListener('keydown', function (e) {
|
||||||
@@ -22,4 +22,422 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── Slug auto-génération ────────────────────────────────────────────────
|
||||||
|
const titleInput = document.getElementById('title');
|
||||||
|
const slugField = document.getElementById('slug');
|
||||||
|
const slugPreview = document.getElementById('slug-preview');
|
||||||
|
|
||||||
|
if (titleInput && slugField) {
|
||||||
|
if (slugField.value !== '') slugField._auto = false;
|
||||||
|
|
||||||
|
titleInput.addEventListener('input', function () {
|
||||||
|
if (slugField._auto !== false) {
|
||||||
|
const generated = slugify(this.value);
|
||||||
|
slugField.value = generated;
|
||||||
|
if (slugPreview) slugPreview.textContent = generated;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
slugField.addEventListener('input', function () {
|
||||||
|
this._auto = (this.value === '');
|
||||||
|
if (slugPreview) slugPreview.textContent = this.value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function slugify(s) {
|
||||||
|
const map = {'à':'a','â':'a','ä':'a','é':'e','è':'e','ê':'e','ë':'e','î':'i','ï':'i','ô':'o','ö':'o','ù':'u','û':'u','ü':'u','ç':'c','æ':'ae','œ':'oe'};
|
||||||
|
return s.toLowerCase().replace(/[àâäéèêëîïôöùûüçæœ]/g, c => map[c] || c)
|
||||||
|
.replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Aperçu couleur catégorie ────────────────────────────────────────────
|
||||||
|
const KNOWN_CATS = {
|
||||||
|
'actualité': 10, 'travaux': 35, 'scolaire': 55,
|
||||||
|
'linux': 120, 'domotique': 160, 'télécom': 190,
|
||||||
|
'blog': 220, 'informatique': 255, 'réflexion': 285,
|
||||||
|
'loisirs': 320, 'perso': 345,
|
||||||
|
};
|
||||||
|
const FREE_HUES = [87, 140, 205, 237, 302];
|
||||||
|
|
||||||
|
function gradient(hue) {
|
||||||
|
return `linear-gradient(135deg,hsl(${hue},70%,88%) 0%,hsl(${hue},60%,28%) 100%)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hashHue(str) {
|
||||||
|
let h = 5381;
|
||||||
|
for (let i = 0; i < str.length; i++) h = (((h << 5) + h) + str.charCodeAt(i)) | 0;
|
||||||
|
return ((Math.abs(h) * 0.6180339887) * 360 | 0) % 360;
|
||||||
|
}
|
||||||
|
|
||||||
|
function nearestKnown(hue) {
|
||||||
|
let best = null, bestDist = Infinity;
|
||||||
|
for (const [name, h] of Object.entries(KNOWN_CATS)) {
|
||||||
|
const d = Math.min(Math.abs(hue - h), 360 - Math.abs(hue - h));
|
||||||
|
if (d < bestDist) { bestDist = d; best = name; }
|
||||||
|
}
|
||||||
|
return { name: best, dist: bestDist };
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateCatPreview(val) {
|
||||||
|
const key = val.trim().toLowerCase();
|
||||||
|
const swatch = document.getElementById('cat-swatch');
|
||||||
|
const hint = document.getElementById('cat-hint');
|
||||||
|
const freeEl = document.getElementById('cat-free-swatches');
|
||||||
|
if (!swatch) return;
|
||||||
|
freeEl.innerHTML = '';
|
||||||
|
|
||||||
|
if (!key) {
|
||||||
|
swatch.style.background = '#e5e7eb';
|
||||||
|
swatch.title = '';
|
||||||
|
hint.textContent = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (KNOWN_CATS[key] !== undefined) {
|
||||||
|
const hue = KNOWN_CATS[key];
|
||||||
|
swatch.style.background = gradient(hue);
|
||||||
|
swatch.title = `${hue}°`;
|
||||||
|
hint.textContent = `Catégorie existante · teinte fixe (${hue}°)`;
|
||||||
|
hint.className = 'text-muted d-block mt-1';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hue = hashHue(key);
|
||||||
|
const { name, dist } = nearestKnown(hue);
|
||||||
|
|
||||||
|
swatch.style.background = gradient(hue);
|
||||||
|
swatch.title = `${hue}°`;
|
||||||
|
|
||||||
|
if (dist < 20) {
|
||||||
|
hint.innerHTML = `⚠ Teinte proche de <strong>${name}</strong> (${dist}° d'écart) · couleurs disponibles :`;
|
||||||
|
hint.className = 'text-warning d-block mt-1';
|
||||||
|
FREE_HUES.forEach(h => {
|
||||||
|
const el = document.createElement('span');
|
||||||
|
el.title = `${h}°`;
|
||||||
|
el.style.cssText = `display:inline-block;width:28px;height:20px;border-radius:4px;cursor:help;background:${gradient(h)}`;
|
||||||
|
freeEl.appendChild(el);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
hint.textContent = `Nouvelle catégorie · teinte libre (${hue}°)`;
|
||||||
|
hint.className = 'text-muted d-block mt-1';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const catInput = document.getElementById('category');
|
||||||
|
if (catInput) {
|
||||||
|
catInput.addEventListener('input', function () { updateCatPreview(this.value); });
|
||||||
|
updateCatPreview(catInput.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Copier la référence Markdown ────────────────────────────────────────
|
||||||
|
document.querySelectorAll('[data-copy-md-name]').forEach(function (btn) {
|
||||||
|
btn.addEventListener('click', function () {
|
||||||
|
const name = this.dataset.copyMdName;
|
||||||
|
const isImage = this.dataset.copyMdIsImage === '1';
|
||||||
|
const ref = isImage ? `` : `[${name}](${name})`;
|
||||||
|
navigator.clipboard.writeText(ref).then(() => {
|
||||||
|
const orig = this.textContent;
|
||||||
|
this.textContent = 'Copié !';
|
||||||
|
setTimeout(() => { this.textContent = orig; }, 1500);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── Boîtes de confirmation (suppression) ───────────────────────────────
|
||||||
|
document.querySelectorAll('button[data-confirm], a[data-confirm]').forEach(function (el) {
|
||||||
|
el.addEventListener('click', function (e) {
|
||||||
|
if (!confirm(this.dataset.confirm)) e.preventDefault();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
document.querySelectorAll('form[data-confirm]').forEach(function (form) {
|
||||||
|
form.addEventListener('submit', function (e) {
|
||||||
|
if (!confirm(this.dataset.confirm)) e.preventDefault();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── Insérer une référence Markdown au curseur ───────────────────────────
|
||||||
|
const ta = document.getElementById('content');
|
||||||
|
if (ta) {
|
||||||
|
ta._savedStart = null;
|
||||||
|
ta._savedEnd = null;
|
||||||
|
|
||||||
|
function saveCursor() {
|
||||||
|
if (document.activeElement === ta) {
|
||||||
|
ta._savedStart = ta.selectionStart;
|
||||||
|
ta._savedEnd = ta.selectionEnd;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.addEventListener('mousedown', saveCursor);
|
||||||
|
ta.addEventListener('keyup', saveCursor);
|
||||||
|
ta.addEventListener('mouseup', saveCursor);
|
||||||
|
|
||||||
|
document.querySelectorAll('[data-insert-ref]').forEach(function (el) {
|
||||||
|
el.addEventListener('click', function () {
|
||||||
|
insertRef(this.dataset.insertRef);
|
||||||
|
});
|
||||||
|
if (el.tagName === 'IMG') {
|
||||||
|
el.addEventListener('mouseenter', function () { this.style.borderColor = '#0d6efd'; });
|
||||||
|
el.addEventListener('mouseleave', function () { this.style.borderColor = 'transparent'; });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function insertRef(url) {
|
||||||
|
if (!ta) return;
|
||||||
|
const isImage = /\.(jpe?g|png|gif|webp|svg|avif)(\?.*)?$/i.test(url);
|
||||||
|
const label = url.startsWith('http')
|
||||||
|
? (decodeURIComponent(url.split('/').pop().split('?')[0]) || url)
|
||||||
|
: url;
|
||||||
|
const ref = isImage ? `` : `[${label}](${url})`;
|
||||||
|
const len = ta.value.length;
|
||||||
|
const start = ta._savedStart !== null ? ta._savedStart : len;
|
||||||
|
const end = ta._savedEnd !== null ? ta._savedEnd : len;
|
||||||
|
ta.focus();
|
||||||
|
ta.setRangeText(ref, start, end, 'end');
|
||||||
|
ta._savedStart = ta._savedEnd = start + ref.length;
|
||||||
|
ta.dispatchEvent(new Event('input'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Compteurs SEO ───────────────────────────────────────────────────────
|
||||||
|
function initCounter(inputId, counterId, max) {
|
||||||
|
const input = document.getElementById(inputId);
|
||||||
|
const counter = document.getElementById(counterId);
|
||||||
|
if (!input || !counter) return;
|
||||||
|
function update() {
|
||||||
|
const len = input.value.length;
|
||||||
|
counter.textContent = `${len} / ${max}`;
|
||||||
|
counter.className = len > max ? 'text-danger' : 'text-muted';
|
||||||
|
}
|
||||||
|
input.addEventListener('input', update);
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
initCounter('seo_title', 'seo_title_counter', 60);
|
||||||
|
initCounter('seo_description', 'seo_desc_counter', 155);
|
||||||
|
|
||||||
|
// ─── Page catégories ─────────────────────────────────────────────────────
|
||||||
|
function catComputeGradient(val) {
|
||||||
|
const key = val.trim().toLowerCase();
|
||||||
|
if (!key) return null;
|
||||||
|
if (KNOWN_CATS[key] !== undefined) return { hue: KNOWN_CATS[key], known: true };
|
||||||
|
const hue = hashHue(key);
|
||||||
|
const { name, dist } = nearestKnown(hue);
|
||||||
|
return { hue, known: false, conflict: dist < 20 ? name : null };
|
||||||
|
}
|
||||||
|
|
||||||
|
document.querySelectorAll('form[action="/?action=rename_category"] input[name="new"]').forEach(function (input) {
|
||||||
|
input.addEventListener('input', function () {
|
||||||
|
const swatch = input.closest('form').querySelector('.rename-swatch');
|
||||||
|
const result = catComputeGradient(input.value);
|
||||||
|
if (swatch) swatch.style.background = result ? gradient(result.hue) : '#e5e7eb';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const newCatInput = document.getElementById('new-cat-input');
|
||||||
|
if (newCatInput) {
|
||||||
|
newCatInput.addEventListener('input', function () {
|
||||||
|
const swatch = document.getElementById('new-cat-swatch');
|
||||||
|
const hint = document.getElementById('new-cat-hint');
|
||||||
|
const result = catComputeGradient(this.value);
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
swatch.style.background = '#e5e7eb';
|
||||||
|
hint.textContent = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
swatch.style.background = gradient(result.hue);
|
||||||
|
|
||||||
|
if (result.known) {
|
||||||
|
hint.textContent = `Catégorie existante · teinte fixe (${result.hue}°)`;
|
||||||
|
hint.className = 'text-muted d-block mb-3';
|
||||||
|
} else if (result.conflict) {
|
||||||
|
hint.textContent = `⚠ Teinte proche de « ${result.conflict} » — choisissez un autre nom ou une couleur disponible ci-dessous`;
|
||||||
|
hint.className = 'text-warning d-block mb-3';
|
||||||
|
} else {
|
||||||
|
hint.textContent = `Couleur libre · teinte ${result.hue}°`;
|
||||||
|
hint.className = 'text-success d-block mb-3';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Import image : récupérer les métadonnées ────────────────────────────
|
||||||
|
const fetchMetaBtn = document.getElementById('fetch-meta-btn');
|
||||||
|
if (fetchMetaBtn) {
|
||||||
|
fetchMetaBtn.addEventListener('click', async function () {
|
||||||
|
const urlInput = document.getElementById('import-url');
|
||||||
|
const resultDiv = document.getElementById('meta-result');
|
||||||
|
const url = urlInput ? urlInput.value.trim() : '';
|
||||||
|
|
||||||
|
if (!url) {
|
||||||
|
resultDiv.innerHTML = '<small class="text-danger">Saisissez une URL d\'abord.</small>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchMetaBtn.disabled = true;
|
||||||
|
fetchMetaBtn.textContent = 'Chargement…';
|
||||||
|
resultDiv.innerHTML = '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/?action=fetch_file_meta&url=${encodeURIComponent(url)}`);
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
if (!data.ok) {
|
||||||
|
resultDiv.innerHTML = `<small class="text-danger">${data.error || 'Erreur lors de la récupération.'}</small>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-remplissage dynamique des champs (si vides)
|
||||||
|
const AUTOFILL = {
|
||||||
|
img_author: { keys: ['author', 'credit'], label: 'Auteur / crédit' },
|
||||||
|
img_source: { keys: ['canonical', 'source'], label: 'URL source' },
|
||||||
|
};
|
||||||
|
const autofillKeys = new Set();
|
||||||
|
const autofillNotice = [];
|
||||||
|
for (const [fieldName, cfg] of Object.entries(AUTOFILL)) {
|
||||||
|
const f = document.querySelector(`input[name="${fieldName}"]`);
|
||||||
|
if (!f || f.value) continue;
|
||||||
|
for (const key of cfg.keys) {
|
||||||
|
if (data[key]) {
|
||||||
|
f.value = data[key];
|
||||||
|
autofillKeys.add(key);
|
||||||
|
autofillNotice.push(`<strong>${cfg.label}</strong> : ${data[key]}`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Affichage dynamique de tous les champs retournés
|
||||||
|
const isPdf = (data.mime === 'application/pdf');
|
||||||
|
const isHtml = (data.mime || '').startsWith('text/html');
|
||||||
|
|
||||||
|
const META_ORDER = ['mime','size','pages','page_size','pdf_version',
|
||||||
|
'width','site_name','og_type','language',
|
||||||
|
'title','description','author','subject','keywords',
|
||||||
|
'credit','source','creator','producer','date','camera','copyright',
|
||||||
|
'canonical','og_image'];
|
||||||
|
const META_LABELS = {
|
||||||
|
mime: 'Type', size: 'Taille', width: 'Dimensions',
|
||||||
|
pages: 'Pages', page_size: 'Format', pdf_version: 'Version PDF',
|
||||||
|
site_name: 'Site', og_type: 'Type OG', language: 'Langue',
|
||||||
|
title: isPdf || isHtml ? 'Titre' : 'Titre EXIF/IPTC',
|
||||||
|
author: isPdf || isHtml ? 'Auteur' : 'Auteur EXIF/IPTC',
|
||||||
|
date: isPdf ? 'Créé le' : isHtml ? 'Publié le' : 'Prise de vue',
|
||||||
|
description: 'Description', subject: 'Sujet', keywords: 'Mots-clés',
|
||||||
|
credit: 'Crédit', source: 'Source IPTC',
|
||||||
|
creator: 'Créé avec', producer: 'Produit par',
|
||||||
|
camera: 'Appareil', copyright: 'Copyright',
|
||||||
|
canonical: 'URL canonique', og_image: 'Image OG',
|
||||||
|
};
|
||||||
|
|
||||||
|
function fmtVal(key, val) {
|
||||||
|
if (key === 'size') return (val/1024).toFixed(0) + ' Ko' + (val >= 1048576 ? ` (${(val/1048576).toFixed(1)} Mo)` : '');
|
||||||
|
if (key === 'width') return `${data.width} × ${data.height} px`;
|
||||||
|
if (key === 'og_image') return `<img src="${val}" style="max-width:120px;max-height:80px;border-radius:4px" alt="">`;
|
||||||
|
if (key === 'canonical') return `<a href="${val}" target="_blank" rel="noopener">${val}</a>`;
|
||||||
|
return String(val);
|
||||||
|
}
|
||||||
|
|
||||||
|
const SKIP = new Set(['ok', 'height']);
|
||||||
|
const seen = new Set();
|
||||||
|
const rows = [];
|
||||||
|
|
||||||
|
for (const key of META_ORDER) {
|
||||||
|
const val = data[key];
|
||||||
|
if (val == null || val === '' || key === 'height') continue;
|
||||||
|
seen.add(key);
|
||||||
|
const badge = autofillKeys.has(key)
|
||||||
|
? ' <span class="badge text-bg-primary ms-1" title="Pré-rempli dans le formulaire">↓ pré-rempli</span>'
|
||||||
|
: '';
|
||||||
|
rows.push([META_LABELS[key] ?? key, fmtVal(key, val) + badge]);
|
||||||
|
}
|
||||||
|
for (const [key, val] of Object.entries(data)) {
|
||||||
|
if (seen.has(key) || SKIP.has(key) || val == null || val === '') continue;
|
||||||
|
rows.push([key, fmtVal(key, val)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
let html = '';
|
||||||
|
if (rows.length > 0) {
|
||||||
|
const trs = rows.map(([k, v]) =>
|
||||||
|
`<tr><th class="text-muted fw-normal pe-3 text-nowrap">${k}</th><td>${v}</td></tr>`
|
||||||
|
).join('');
|
||||||
|
html = `<table class="table table-sm table-borderless mb-0 small"><tbody>${trs}</tbody></table>`;
|
||||||
|
} else {
|
||||||
|
html = '<small class="text-muted">Aucune métadonnée disponible pour ce fichier.</small>';
|
||||||
|
}
|
||||||
|
if (autofillNotice.length > 0) {
|
||||||
|
html += `<div class="small text-primary mt-1">✓ Pré-rempli — ${autofillNotice.join(' · ')}</div>`;
|
||||||
|
}
|
||||||
|
resultDiv.innerHTML = html;
|
||||||
|
} catch {
|
||||||
|
resultDiv.innerHTML = '<small class="text-danger">Erreur de connexion.</small>';
|
||||||
|
} finally {
|
||||||
|
fetchMetaBtn.disabled = false;
|
||||||
|
fetchMetaBtn.textContent = 'Métadonnées';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Import image : toggle mode download ────────────────────────────────
|
||||||
|
document.querySelectorAll('input[name="mode"]').forEach(function (r) {
|
||||||
|
r.addEventListener('change', function () {
|
||||||
|
const dl = this.value === 'download';
|
||||||
|
const ss = this.value === 'screenshot';
|
||||||
|
const warn = document.getElementById('copyright-warning');
|
||||||
|
const fields = document.getElementById('download-fields');
|
||||||
|
if (warn) warn.style.display = dl ? 'block' : 'none';
|
||||||
|
if (fields) fields.style.display = (dl || ss) ? 'block' : 'none';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── Données page (mode édition uniquement) ──────────────────────────────
|
||||||
|
const pageEl = document.getElementById('vl-page');
|
||||||
|
if (!pageEl) return;
|
||||||
|
|
||||||
|
const uuid = pageEl.dataset.uuid;
|
||||||
|
const insertUrl = pageEl.dataset.insertUrl;
|
||||||
|
|
||||||
|
// Auto-insertion après import d'image
|
||||||
|
if (insertUrl && ta) {
|
||||||
|
const isImage = /\.(jpe?g|png|gif|webp|svg|avif)(\?.*)?$/i.test(insertUrl);
|
||||||
|
const name = decodeURIComponent(insertUrl.split('/').pop().split('?')[0]) || 'fichier';
|
||||||
|
const ref = isImage ? `` : `[${name}](${insertUrl})`;
|
||||||
|
const sep = ta.value.length > 0 && !ta.value.endsWith('\n') ? '\n' : '';
|
||||||
|
ta.value += sep + ref;
|
||||||
|
ta.focus();
|
||||||
|
ta.selectionStart = ta.selectionEnd = ta.value.length;
|
||||||
|
ta.dispatchEvent(new Event('input'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Autosave ────────────────────────────────────────────────────────────
|
||||||
|
const indicator = document.getElementById('autosave-indicator');
|
||||||
|
if (!indicator || !uuid) return;
|
||||||
|
|
||||||
|
let timer = null;
|
||||||
|
|
||||||
|
function scheduleAutosave() {
|
||||||
|
clearTimeout(timer);
|
||||||
|
timer = setTimeout(doAutosave, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doAutosave() {
|
||||||
|
const title = document.getElementById('title').value;
|
||||||
|
const slug = document.getElementById('slug').value;
|
||||||
|
const content = document.getElementById('content').value;
|
||||||
|
indicator.textContent = 'Sauvegarde…';
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/?action=autosave&uuid=${encodeURIComponent(uuid)}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
||||||
|
body: new URLSearchParams({title, slug, content}),
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
indicator.textContent = data.ok ? `Brouillon sauvegardé à ${data.time}` : 'Erreur de sauvegarde';
|
||||||
|
} catch {
|
||||||
|
indicator.textContent = 'Erreur de sauvegarde';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
['title', 'slug', 'content'].forEach(id => {
|
||||||
|
document.getElementById(id)?.addEventListener('input', scheduleAutosave);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -142,9 +142,20 @@ if (!$email) {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Nom d'affichage depuis les claims SSO
|
||||||
|
$displayName = '';
|
||||||
|
if (!empty($claims['given_name']) || !empty($claims['family_name'])) {
|
||||||
|
$displayName = trim(($claims['given_name'] ?? '') . ' ' . ($claims['family_name'] ?? ''));
|
||||||
|
} elseif (!empty($claims['name'])) {
|
||||||
|
$displayName = trim($claims['name']);
|
||||||
|
} elseif (!empty($claims['preferred_username'])) {
|
||||||
|
$displayName = trim($claims['preferred_username']);
|
||||||
|
}
|
||||||
|
|
||||||
// Ouvre la session authentifiée
|
// Ouvre la session authentifiée
|
||||||
session_regenerate_id(true);
|
session_regenerate_id(true);
|
||||||
$_SESSION['user_email'] = strtolower(trim($email));
|
$_SESSION['user_email'] = strtolower(trim($email));
|
||||||
|
$_SESSION['user_display_name'] = $displayName;
|
||||||
$_SESSION['oidc'] = [
|
$_SESSION['oidc'] = [
|
||||||
'issuer' => $OIDC_ISSUER,
|
'issuer' => $OIDC_ISSUER,
|
||||||
'sub' => $claims['sub'] ?? null,
|
'sub' => $claims['sub'] ?? null,
|
||||||
@@ -153,6 +164,23 @@ $_SESSION['oidc'] = [
|
|||||||
'expires_at' => time() + (int)($tokens['expires_in'] ?? 3600),
|
'expires_at' => time() + (int)($tokens['expires_in'] ?? 3600),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Persiste le nom d'affichage en base (seulement s'il vient du SSO et que la table existe)
|
||||||
|
if ($displayName !== '') {
|
||||||
|
require_once dirname(__DIR__, 2) . '/src/auth.php';
|
||||||
|
$pdo = dbPdo();
|
||||||
|
if ($pdo) {
|
||||||
|
try {
|
||||||
|
$st = $pdo->prepare(
|
||||||
|
'INSERT INTO user_profiles (email, display_name, updated_at)
|
||||||
|
VALUES (:e, :n, now())
|
||||||
|
ON CONFLICT (email) DO NOTHING'
|
||||||
|
);
|
||||||
|
$st->execute([':e' => strtolower(trim($email)), ':n' => $displayName]);
|
||||||
|
} catch (\Throwable) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$target = $_SESSION['oidc_return_to'] ?? '/';
|
$target = $_SESSION['oidc_return_to'] ?? '/';
|
||||||
unset($_SESSION['oidc_return_to'], $_SESSION['oidc_flow']);
|
unset($_SESSION['oidc_return_to'], $_SESSION['oidc_flow']);
|
||||||
if (!is_string($target) || $target === '' || $target[0] !== '/') {
|
if (!is_string($target) || $target === '' || $target[0] !== '/') {
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
class ArticleManager
|
class ArticleManager
|
||||||
{
|
{
|
||||||
|
private const MAX_REVISIONS = 50;
|
||||||
|
|
||||||
public function __construct(private string $dataDir)
|
public function __construct(private string $dataDir)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
@@ -65,11 +67,24 @@ class ArticleManager
|
|||||||
return $this->loadArticle($dir);
|
return $this->loadArticle($dir);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getRevisionContent(string $uuid, int $n): ?string
|
||||||
|
{
|
||||||
|
if (!$this->isValidUuid($uuid) || $n < 1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$path = sprintf('%s/%s/revisions/%04d.md', $this->dataDir, $uuid, $n);
|
||||||
|
if (!file_exists($path)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$c = file_get_contents($path);
|
||||||
|
return $c !== false ? $c : null;
|
||||||
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------ //
|
// ------------------------------------------------------------------ //
|
||||||
// Écriture
|
// Écriture
|
||||||
// ------------------------------------------------------------------ //
|
// ------------------------------------------------------------------ //
|
||||||
|
|
||||||
public function create(string $title, string $content, bool $published, string $slug = '', string $publishedAt = '', string $author = '', string $seoTitle = '', string $seoDescription = '', string $ogImage = ''): string
|
public function create(string $title, string $content, bool $published, string $slug = '', string $publishedAt = '', string $author = '', string $seoTitle = '', string $seoDescription = '', string $ogImage = '', string $category = ''): string
|
||||||
{
|
{
|
||||||
$uuid = $this->generateUuid();
|
$uuid = $this->generateUuid();
|
||||||
$slug = $slug !== '' ? $this->sanitizeSlug($slug) : $this->generateSlug($title);
|
$slug = $slug !== '' ? $this->sanitizeSlug($slug) : $this->generateSlug($title);
|
||||||
@@ -91,9 +106,13 @@ class ArticleManager
|
|||||||
'created_at' => $now,
|
'created_at' => $now,
|
||||||
'updated_at' => $now,
|
'updated_at' => $now,
|
||||||
'revisions' => [],
|
'revisions' => [],
|
||||||
|
'cover' => '',
|
||||||
|
'files_meta' => [],
|
||||||
|
'external_links' => [],
|
||||||
'seo_title' => $seoTitle,
|
'seo_title' => $seoTitle,
|
||||||
'seo_description' => $seoDescription,
|
'seo_description' => $seoDescription,
|
||||||
'og_image' => $ogImage,
|
'og_image' => $ogImage,
|
||||||
|
'category' => $category,
|
||||||
];
|
];
|
||||||
$this->writeMeta($dir, $meta);
|
$this->writeMeta($dir, $meta);
|
||||||
file_put_contents($dir . '/index.md', ltrim($content));
|
file_put_contents($dir . '/index.md', ltrim($content));
|
||||||
@@ -101,18 +120,37 @@ class ArticleManager
|
|||||||
return $uuid;
|
return $uuid;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function update(string $uuid, string $title, string $content, bool $published, string $slug, string $publishedAt, string $revisionComment = '', string $seoTitle = '', string $seoDescription = '', string $ogImage = ''): void
|
public function update(string $uuid, string $title, string $content, bool $published, string $slug, string $publishedAt, string $revisionComment = '', string $seoTitle = '', string $seoDescription = '', string $ogImage = '', string $category = ''): void
|
||||||
{
|
{
|
||||||
$article = $this->getByUuid($uuid);
|
$article = $this->getByUuid($uuid);
|
||||||
if (!$article) {
|
if (!$article) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$slug = $slug !== '' ? $this->sanitizeSlug($slug) : $this->generateSlug($title);
|
$slug = $slug !== '' ? $this->sanitizeSlug($slug) : $this->generateSlug($title);
|
||||||
$slug = $this->uniqueSlug($slug, $uuid);
|
$slug = $this->uniqueSlug($slug, $uuid);
|
||||||
|
|
||||||
|
// Snapshot de l'état courant avant écrasement
|
||||||
$revisions = $article['revisions'] ?? [];
|
$revisions = $article['revisions'] ?? [];
|
||||||
if ($revisionComment !== '') {
|
$revDir = $this->dataDir . '/' . $uuid . '/revisions';
|
||||||
$revisions[] = ['date' => date('Y-m-d H:i:s'), 'comment' => $revisionComment];
|
if (!is_dir($revDir)) {
|
||||||
|
mkdir($revDir, 0755, true);
|
||||||
|
}
|
||||||
|
$n = count($revisions) + 1;
|
||||||
|
$revFile = sprintf('%s/%04d.md', $revDir, $n);
|
||||||
|
file_put_contents($revFile, $article['content']);
|
||||||
|
|
||||||
|
$revisions[] = [
|
||||||
|
'n' => $n,
|
||||||
|
'date' => date('Y-m-d H:i:s'),
|
||||||
|
'comment' => $revisionComment,
|
||||||
|
'title' => $article['title'],
|
||||||
|
];
|
||||||
|
|
||||||
|
// Limite à MAX_REVISIONS
|
||||||
|
if (count($revisions) > self::MAX_REVISIONS) {
|
||||||
|
$oldest = array_shift($revisions);
|
||||||
|
@unlink(sprintf('%s/%04d.md', $revDir, (int)($oldest['n'] ?? 0)));
|
||||||
}
|
}
|
||||||
|
|
||||||
$meta = [
|
$meta = [
|
||||||
@@ -125,15 +163,377 @@ class ArticleManager
|
|||||||
'created_at' => $article['created_at'] ?? date('Y-m-d H:i:s'),
|
'created_at' => $article['created_at'] ?? date('Y-m-d H:i:s'),
|
||||||
'updated_at' => date('Y-m-d H:i:s'),
|
'updated_at' => date('Y-m-d H:i:s'),
|
||||||
'revisions' => $revisions,
|
'revisions' => $revisions,
|
||||||
|
'cover' => $article['cover'] ?? '',
|
||||||
|
'files_meta' => $article['files_meta'] ?? [],
|
||||||
|
'external_links' => $article['external_links'] ?? [],
|
||||||
'seo_title' => $seoTitle,
|
'seo_title' => $seoTitle,
|
||||||
'seo_description' => $seoDescription,
|
'seo_description' => $seoDescription,
|
||||||
'og_image' => $ogImage,
|
'og_image' => $ogImage,
|
||||||
|
'category' => $category,
|
||||||
];
|
];
|
||||||
$dir = $this->dataDir . '/' . $uuid;
|
$dir = $this->dataDir . '/' . $uuid;
|
||||||
$this->writeMeta($dir, $meta);
|
$this->writeMeta($dir, $meta);
|
||||||
file_put_contents($dir . '/index.md', ltrim($content));
|
file_put_contents($dir . '/index.md', ltrim($content));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function autosave(string $uuid, string $title, string $content, string $slug): bool
|
||||||
|
{
|
||||||
|
if (!$this->isValidUuid($uuid)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$dir = $this->dataDir . '/' . $uuid;
|
||||||
|
$raw = @file_get_contents($dir . '/meta.json');
|
||||||
|
if ($raw === false) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$meta = json_decode($raw, true);
|
||||||
|
if (!is_array($meta)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$slug = $slug !== '' ? $this->sanitizeSlug($slug) : $this->generateSlug($title);
|
||||||
|
$slug = $this->uniqueSlug($slug, $uuid);
|
||||||
|
|
||||||
|
$meta['title'] = $title;
|
||||||
|
$meta['slug'] = $slug;
|
||||||
|
$meta['updated_at'] = date('Y-m-d H:i:s');
|
||||||
|
|
||||||
|
$this->writeMeta($dir, $meta);
|
||||||
|
@file_put_contents($dir . '/index.md', ltrim($content));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addFileMeta(string $uuid, string $filename, string $author, string $sourceUrl, string $title = '', array $extraMeta = []): void
|
||||||
|
{
|
||||||
|
if (!$this->isValidUuid($uuid)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$filename = basename($filename);
|
||||||
|
$path = $this->dataDir . '/' . $uuid . '/files/' . $filename;
|
||||||
|
if (!file_exists($path)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$raw = file_get_contents($this->dataDir . '/' . $uuid . '/meta.json');
|
||||||
|
if ($raw === false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$meta = json_decode($raw, true);
|
||||||
|
if (!is_array($meta)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!isset($meta['files_meta']) || !is_array($meta['files_meta'])) {
|
||||||
|
$meta['files_meta'] = [];
|
||||||
|
}
|
||||||
|
$entry = ['author' => $author, 'source_url' => $sourceUrl];
|
||||||
|
if ($title !== '') {
|
||||||
|
$entry['title'] = $title;
|
||||||
|
}
|
||||||
|
if (!empty($extraMeta)) {
|
||||||
|
$clean = $extraMeta;
|
||||||
|
unset($clean['title'], $clean['author'], $clean['credit'], $clean['source']);
|
||||||
|
$entry['meta'] = $clean;
|
||||||
|
}
|
||||||
|
$meta['files_meta'][$filename] = $entry;
|
||||||
|
$this->writeMeta($this->dataDir . '/' . $uuid, $meta);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setCover(string $uuid, string $filename): void
|
||||||
|
{
|
||||||
|
$article = $this->getByUuid($uuid);
|
||||||
|
if (!$article) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$filename = basename($filename);
|
||||||
|
$filesDir = $this->dataDir . '/' . $uuid . '/files';
|
||||||
|
$targetPath = $filesDir . '/' . $filename;
|
||||||
|
if (!file_exists($targetPath)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$mime = mime_content_type($targetPath) ?: '';
|
||||||
|
$ext = $this->extFromMime($mime) ?? strtolower(pathinfo($filename, PATHINFO_EXTENSION)) ?: 'jpg';
|
||||||
|
$coverName = 'cover.' . $ext;
|
||||||
|
|
||||||
|
// Rename old cover back to hash name
|
||||||
|
$oldCover = $article['cover'] ?? '';
|
||||||
|
if ($oldCover && $oldCover !== $filename && $oldCover !== $coverName) {
|
||||||
|
$oldPath = $filesDir . '/' . basename($oldCover);
|
||||||
|
if (file_exists($oldPath)) {
|
||||||
|
$hash = substr(hash_file('sha256', $oldPath), 0, 16);
|
||||||
|
$size = filesize($oldPath);
|
||||||
|
$oldExt = strtolower(pathinfo($oldCover, PATHINFO_EXTENSION));
|
||||||
|
rename($oldPath, $filesDir . '/' . "{$hash}-{$size}.{$oldExt}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rename target to cover.{ext}
|
||||||
|
$newPath = $filesDir . '/' . $coverName;
|
||||||
|
if ($targetPath !== $newPath) {
|
||||||
|
rename($targetPath, $newPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
$raw = file_get_contents($this->dataDir . '/' . $uuid . '/meta.json');
|
||||||
|
if ($raw === false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$meta = json_decode($raw, true);
|
||||||
|
if (!is_array($meta)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$meta['cover'] = $coverName;
|
||||||
|
$this->writeMeta($this->dataDir . '/' . $uuid, $meta);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addFileFromUrl(string $uuid, string $url, bool $isCover = false, string $author = '', string $sourceUrl = '', string $title = '', array $extraMeta = []): ?string
|
||||||
|
{
|
||||||
|
if (!$this->isValidUuid($uuid)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (!filter_var($url, FILTER_VALIDATE_URL) || !preg_match('#^https?://#i', $url)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$ch = curl_init($url);
|
||||||
|
curl_setopt_array($ch, [
|
||||||
|
CURLOPT_RETURNTRANSFER => true,
|
||||||
|
CURLOPT_FOLLOWLOCATION => true,
|
||||||
|
CURLOPT_MAXREDIRS => 3,
|
||||||
|
CURLOPT_TIMEOUT => 20,
|
||||||
|
CURLOPT_CONNECTTIMEOUT => 5,
|
||||||
|
CURLOPT_USERAGENT => 'varlog/1.0',
|
||||||
|
]);
|
||||||
|
$body = curl_exec($ch);
|
||||||
|
$info = curl_getinfo($ch);
|
||||||
|
curl_close($ch);
|
||||||
|
|
||||||
|
if ($body === false || (int)$info['http_code'] !== 200) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$tmp = tempnam(sys_get_temp_dir(), 'vl_');
|
||||||
|
file_put_contents($tmp, $body);
|
||||||
|
|
||||||
|
$mime = mime_content_type($tmp) ?: 'application/octet-stream';
|
||||||
|
$isImage = str_starts_with($mime, 'image/');
|
||||||
|
$filesDir = $this->dataDir . '/' . $uuid . '/files';
|
||||||
|
if (!is_dir($filesDir)) {
|
||||||
|
mkdir($filesDir, 0755, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($isImage) {
|
||||||
|
$ext = $this->extFromMime($mime) ?? strtolower(pathinfo(parse_url($url, PHP_URL_PATH) ?? '', PATHINFO_EXTENSION)) ?: 'jpg';
|
||||||
|
|
||||||
|
if ($isCover) {
|
||||||
|
// Gérer l'ancienne cover
|
||||||
|
$article = $this->getByUuid($uuid);
|
||||||
|
$oldCover = $article['cover'] ?? '';
|
||||||
|
if ($oldCover) {
|
||||||
|
$oldPath = $filesDir . '/' . basename($oldCover);
|
||||||
|
if (file_exists($oldPath)) {
|
||||||
|
$hash = substr(hash_file('sha256', $oldPath), 0, 16);
|
||||||
|
$size = filesize($oldPath);
|
||||||
|
$oldExt = strtolower(pathinfo($oldCover, PATHINFO_EXTENSION));
|
||||||
|
rename($oldPath, $filesDir . '/' . "{$hash}-{$size}.{$oldExt}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$filename = 'cover.' . $ext;
|
||||||
|
} else {
|
||||||
|
$hash = substr(hash_file('sha256', $tmp), 0, 16);
|
||||||
|
$size = strlen($body);
|
||||||
|
$filename = "{$hash}-{$size}.{$ext}";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Non-image : nom extrait de l'URL, sanitisé, dédupliqué
|
||||||
|
$urlPath = parse_url($url, PHP_URL_PATH) ?? '';
|
||||||
|
$filename = preg_replace('/[^a-zA-Z0-9._-]/', '_', basename($urlPath)) ?: 'file';
|
||||||
|
$i = 1;
|
||||||
|
$info = pathinfo($filename);
|
||||||
|
while (file_exists($filesDir . '/' . $filename)) {
|
||||||
|
$filename = $info['filename'] . '_' . $i . (isset($info['extension']) ? '.' . $info['extension'] : '');
|
||||||
|
$i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rename($tmp, $filesDir . '/' . $filename);
|
||||||
|
|
||||||
|
if ($author !== '' || $sourceUrl !== '' || $title !== '' || !empty($extraMeta)) {
|
||||||
|
$this->addFileMeta($uuid, $filename, $author, $sourceUrl, $title, $extraMeta);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($isCover && $isImage) {
|
||||||
|
$raw = file_get_contents($this->dataDir . '/' . $uuid . '/meta.json');
|
||||||
|
if ($raw !== false) {
|
||||||
|
$meta = json_decode($raw, true);
|
||||||
|
if (is_array($meta)) {
|
||||||
|
$meta['cover'] = $filename;
|
||||||
|
$this->writeMeta($this->dataDir . '/' . $uuid, $meta);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $filename;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addExternalLink(string $uuid, string $url, string $title = '', string $author = '', array $extraMeta = []): bool
|
||||||
|
{
|
||||||
|
if (!$this->isValidUuid($uuid) || !filter_var($url, FILTER_VALIDATE_URL)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$dir = $this->dataDir . '/' . $uuid;
|
||||||
|
$raw = file_get_contents($dir . '/meta.json');
|
||||||
|
if ($raw === false) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$meta = json_decode($raw, true);
|
||||||
|
if (!is_array($meta)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!isset($meta['external_links']) || !is_array($meta['external_links'])) {
|
||||||
|
$meta['external_links'] = [];
|
||||||
|
}
|
||||||
|
foreach ($meta['external_links'] as $link) {
|
||||||
|
if ($link['url'] === $url) {
|
||||||
|
return true; // already exists
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$urlPath = parse_url($url, PHP_URL_PATH) ?? '';
|
||||||
|
$name = $title !== '' ? $title : (rawurldecode(basename($urlPath)) ?: $url);
|
||||||
|
$entry = ['url' => $url, 'name' => $name, 'added_at' => date('Y-m-d H:i:s')];
|
||||||
|
$resolvedAuthor = $author !== '' ? $author : ($extraMeta['author'] ?? '');
|
||||||
|
if ($resolvedAuthor !== '') {
|
||||||
|
$entry['author'] = $resolvedAuthor;
|
||||||
|
}
|
||||||
|
if (!empty($extraMeta)) {
|
||||||
|
$clean = $extraMeta;
|
||||||
|
unset($clean['title'], $clean['author'], $clean['credit']);
|
||||||
|
$entry['meta'] = $clean;
|
||||||
|
}
|
||||||
|
$meta['external_links'][] = $entry;
|
||||||
|
$this->writeMeta($dir, $meta);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updateExternalLinkMeta(string $uuid, string $url, array $metaUpdates): bool
|
||||||
|
{
|
||||||
|
if (!$this->isValidUuid($uuid)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$dir = $this->dataDir . '/' . $uuid;
|
||||||
|
$raw = file_get_contents($dir . '/meta.json');
|
||||||
|
if ($raw === false) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$meta = json_decode($raw, true);
|
||||||
|
if (!is_array($meta)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$found = false;
|
||||||
|
foreach ($meta['external_links'] as &$link) {
|
||||||
|
if ($link['url'] === $url) {
|
||||||
|
$link['meta'] = array_merge($link['meta'] ?? [], $metaUpdates);
|
||||||
|
$found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
unset($link);
|
||||||
|
if (!$found) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$this->writeMeta($dir, $meta);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function removeExternalLink(string $uuid, string $url): bool
|
||||||
|
{
|
||||||
|
if (!$this->isValidUuid($uuid)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$dir = $this->dataDir . '/' . $uuid;
|
||||||
|
$raw = file_get_contents($dir . '/meta.json');
|
||||||
|
if ($raw === false) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$meta = json_decode($raw, true);
|
||||||
|
if (!is_array($meta)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$meta['external_links'] = array_values(array_filter(
|
||||||
|
$meta['external_links'] ?? [],
|
||||||
|
static fn ($l) => $l['url'] !== $url
|
||||||
|
));
|
||||||
|
$this->writeMeta($dir, $meta);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCategories(): array
|
||||||
|
{
|
||||||
|
$cats = [];
|
||||||
|
foreach ($this->getAll() as $article) {
|
||||||
|
$cat = trim($article['category'] ?? '');
|
||||||
|
if ($cat !== '') {
|
||||||
|
$cats[$cat] = ($cats[$cat] ?? 0) + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ksort($cats);
|
||||||
|
return $cats;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function renameCategory(string $old, string $new): void
|
||||||
|
{
|
||||||
|
if (!is_dir($this->dataDir)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
foreach (scandir($this->dataDir) as $entry) {
|
||||||
|
if ($entry === '.' || $entry === '..') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$metaPath = $this->dataDir . '/' . $entry . '/meta.json';
|
||||||
|
if (!file_exists($metaPath)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$raw = file_get_contents($metaPath);
|
||||||
|
if ($raw === false) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$meta = json_decode($raw, true);
|
||||||
|
if (!is_array($meta) || trim($meta['category'] ?? '') !== $old) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$meta['category'] = $new;
|
||||||
|
$this->writeMeta($this->dataDir . '/' . $entry, $meta);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function deleteCategory(string $name): void
|
||||||
|
{
|
||||||
|
$this->renameCategory($name, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPrivateCategories(): array
|
||||||
|
{
|
||||||
|
$path = $this->dataDir . '/private_cats.json';
|
||||||
|
if (!file_exists($path)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
$data = json_decode((string)file_get_contents($path), true);
|
||||||
|
return is_array($data) ? $data : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function togglePrivateCategory(string $cat): void
|
||||||
|
{
|
||||||
|
$cats = $this->getPrivateCategories();
|
||||||
|
if (in_array($cat, $cats, true)) {
|
||||||
|
$cats = array_values(array_filter($cats, fn ($c) => $c !== $cat));
|
||||||
|
} else {
|
||||||
|
$cats[] = $cat;
|
||||||
|
}
|
||||||
|
file_put_contents(
|
||||||
|
$this->dataDir . '/private_cats.json',
|
||||||
|
json_encode(array_values($cats), JSON_UNESCAPED_UNICODE)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public function delete(string $uuid): void
|
public function delete(string $uuid): void
|
||||||
{
|
{
|
||||||
if (!$this->isValidUuid($uuid)) {
|
if (!$this->isValidUuid($uuid)) {
|
||||||
@@ -205,6 +605,24 @@ class ArticleManager
|
|||||||
mkdir($dir, 0755, true);
|
mkdir($dir, 0755, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$mime = mime_content_type($uploadedFile['tmp_name']) ?: 'application/octet-stream';
|
||||||
|
|
||||||
|
if (str_starts_with($mime, 'image/')) {
|
||||||
|
$ext = $this->extFromMime($mime)
|
||||||
|
?? strtolower(pathinfo($uploadedFile['name'], PATHINFO_EXTENSION))
|
||||||
|
?: 'jpg';
|
||||||
|
$hash = substr(hash_file('sha256', $uploadedFile['tmp_name']), 0, 16);
|
||||||
|
$size = $uploadedFile['size'] ?? filesize($uploadedFile['tmp_name']);
|
||||||
|
$name = "{$hash}-{$size}.{$ext}";
|
||||||
|
$dest = $dir . '/' . $name;
|
||||||
|
// Même hash = même contenu : pas de collision réelle
|
||||||
|
if (!move_uploaded_file($uploadedFile['tmp_name'], $dest)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return $name;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Non-image : nom sanitisé + déduplication
|
||||||
$name = preg_replace('/[^a-zA-Z0-9._-]/', '_', basename($uploadedFile['name']));
|
$name = preg_replace('/[^a-zA-Z0-9._-]/', '_', basename($uploadedFile['name']));
|
||||||
$dest = $dir . '/' . $name;
|
$dest = $dir . '/' . $name;
|
||||||
$i = 1;
|
$i = 1;
|
||||||
@@ -213,7 +631,6 @@ class ArticleManager
|
|||||||
$dest = $dir . '/' . $info['filename'] . '_' . $i . (isset($info['extension']) ? '.' . $info['extension'] : '');
|
$dest = $dir . '/' . $info['filename'] . '_' . $i . (isset($info['extension']) ? '.' . $info['extension'] : '');
|
||||||
$i++;
|
$i++;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!move_uploaded_file($uploadedFile['tmp_name'], $dest)) {
|
if (!move_uploaded_file($uploadedFile['tmp_name'], $dest)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -221,7 +638,7 @@ class ArticleManager
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------ //
|
// ------------------------------------------------------------------ //
|
||||||
// Rendu : résout les chemins relatifs dans le contenu Markdown
|
// Rendu
|
||||||
// ------------------------------------------------------------------ //
|
// ------------------------------------------------------------------ //
|
||||||
|
|
||||||
public function resolveFileUrls(string $uuid, string $markdown): string
|
public function resolveFileUrls(string $uuid, string $markdown): string
|
||||||
@@ -241,6 +658,18 @@ class ArticleManager
|
|||||||
// Helpers privés
|
// Helpers privés
|
||||||
// ------------------------------------------------------------------ //
|
// ------------------------------------------------------------------ //
|
||||||
|
|
||||||
|
private function extFromMime(string $mime): ?string
|
||||||
|
{
|
||||||
|
return match($mime) {
|
||||||
|
'image/jpeg' => 'jpg',
|
||||||
|
'image/png' => 'png',
|
||||||
|
'image/webp' => 'webp',
|
||||||
|
'image/gif' => 'gif',
|
||||||
|
'image/avif' => 'avif',
|
||||||
|
default => null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private function loadArticle(string $dir): ?array
|
private function loadArticle(string $dir): ?array
|
||||||
{
|
{
|
||||||
$metaPath = $dir . '/meta.json';
|
$metaPath = $dir . '/meta.json';
|
||||||
@@ -255,8 +684,17 @@ class ArticleManager
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
$meta['content'] = file_exists($contentPath) ? (string)file_get_contents($contentPath) : '';
|
$meta['content'] = file_exists($contentPath) ? (string)file_get_contents($contentPath) : '';
|
||||||
$meta['published'] = (bool)($meta['published'] ?? false);
|
$meta['published'] = (bool)($meta['published'] ?? false);
|
||||||
|
$meta['files_meta'] = $meta['files_meta'] ?? [];
|
||||||
|
$meta['external_links'] = $meta['external_links'] ?? [];
|
||||||
|
|
||||||
|
if (!empty($meta['cover'])) {
|
||||||
|
$coverPath = $dir . '/files/' . basename((string)$meta['cover']);
|
||||||
|
if (!file_exists($coverPath)) {
|
||||||
|
$meta['cover'] = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return $meta;
|
return $meta;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
class RatingManager
|
||||||
|
{
|
||||||
|
public function __construct(private PDO $pdo)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rate(string $uuid, string $email, int $rating): void
|
||||||
|
{
|
||||||
|
$st = $this->pdo->prepare(
|
||||||
|
'INSERT INTO article_ratings (article_uuid, user_email, rating)
|
||||||
|
VALUES (:uuid, :email, :r)
|
||||||
|
ON CONFLICT (article_uuid, user_email)
|
||||||
|
DO UPDATE SET rating = :r, rated_at = NOW()'
|
||||||
|
);
|
||||||
|
$st->execute([':uuid' => $uuid, ':email' => strtolower($email), ':r' => $rating]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array{avg: float|null, count: int} */
|
||||||
|
public function statsForArticle(string $uuid): array
|
||||||
|
{
|
||||||
|
$st = $this->pdo->prepare(
|
||||||
|
'SELECT ROUND(AVG(rating)::numeric, 1) as avg, COUNT(*) as count
|
||||||
|
FROM article_ratings WHERE article_uuid = :uuid'
|
||||||
|
);
|
||||||
|
$st->execute([':uuid' => $uuid]);
|
||||||
|
$row = $st->fetch(PDO::FETCH_ASSOC);
|
||||||
|
return [
|
||||||
|
'avg' => $row && $row['avg'] !== null ? (float)$row['avg'] : null,
|
||||||
|
'count' => $row ? (int)$row['count'] : 0,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function userRating(string $uuid, string $email): ?int
|
||||||
|
{
|
||||||
|
$st = $this->pdo->prepare(
|
||||||
|
'SELECT rating FROM article_ratings WHERE article_uuid = :uuid AND user_email = :email'
|
||||||
|
);
|
||||||
|
$st->execute([':uuid' => $uuid, ':email' => strtolower($email)]);
|
||||||
|
$v = $st->fetchColumn();
|
||||||
|
return $v !== false ? (int)$v : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,15 +21,186 @@ function currentUserEmail(): ?string
|
|||||||
return $_SESSION['user_email'] ?? null;
|
return $_SESSION['user_email'] ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function currentUserName(): string
|
||||||
|
{
|
||||||
|
if (!isLoggedIn()) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
if (isset($_SESSION['user_display_name']) && $_SESSION['user_display_name'] !== '') {
|
||||||
|
return $_SESSION['user_display_name'];
|
||||||
|
}
|
||||||
|
$name = authorDisplayName(currentUserEmail() ?? '');
|
||||||
|
$_SESSION['user_display_name'] = $name;
|
||||||
|
return $name;
|
||||||
|
}
|
||||||
|
|
||||||
|
function authorDisplayName(string $email): string
|
||||||
|
{
|
||||||
|
static $cache = [];
|
||||||
|
$key = strtolower(trim($email));
|
||||||
|
if ($key === '') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
if (array_key_exists($key, $cache)) {
|
||||||
|
return $cache[$key];
|
||||||
|
}
|
||||||
|
$pdo = dbPdo();
|
||||||
|
if ($pdo) {
|
||||||
|
try {
|
||||||
|
$st = $pdo->prepare('SELECT display_name FROM user_profiles WHERE email = :e');
|
||||||
|
$st->execute([':e' => $key]);
|
||||||
|
$name = $st->fetchColumn();
|
||||||
|
$cache[$key] = ($name !== false && $name !== '') ? $name : explode('@', $key)[0];
|
||||||
|
return $cache[$key];
|
||||||
|
} catch (\Throwable) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$cache[$key] = explode('@', $key)[0];
|
||||||
|
return $cache[$key];
|
||||||
|
}
|
||||||
|
|
||||||
|
function dbPdo(): ?PDO
|
||||||
|
{
|
||||||
|
static $pdo = null;
|
||||||
|
static $failed = false;
|
||||||
|
if ($failed) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if ($pdo !== null) {
|
||||||
|
return $pdo;
|
||||||
|
}
|
||||||
|
$dsn = $_ENV['DB_DSN'] ?? (getenv('DB_DSN') ?: '');
|
||||||
|
$user = $_ENV['DB_USER'] ?? (getenv('DB_USER') ?: '');
|
||||||
|
$pass = $_ENV['DB_PASS'] ?? (getenv('DB_PASS') ?: '');
|
||||||
|
if (!$dsn) {
|
||||||
|
$failed = true;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
$pdo = new PDO($dsn, $user ?: null, $pass ?: null, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
|
||||||
|
} catch (\Throwable) {
|
||||||
|
$failed = true;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return $pdo;
|
||||||
|
}
|
||||||
|
|
||||||
|
function currentUserRoles(): array
|
||||||
|
{
|
||||||
|
if (!isLoggedIn()) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
if (isset($_SESSION['user_roles'])) {
|
||||||
|
return $_SESSION['user_roles'];
|
||||||
|
}
|
||||||
|
$pdo = dbPdo();
|
||||||
|
if (!$pdo) {
|
||||||
|
$_SESSION['user_roles'] = [];
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
$st = $pdo->prepare(
|
||||||
|
'SELECT r.name FROM roles r
|
||||||
|
JOIN user_roles ur ON ur.role_id = r.id
|
||||||
|
WHERE ur.user_email = :e'
|
||||||
|
);
|
||||||
|
$st->execute([':e' => strtolower(currentUserEmail() ?? '')]);
|
||||||
|
$_SESSION['user_roles'] = $st->fetchAll(PDO::FETCH_COLUMN) ?: [];
|
||||||
|
} catch (\Throwable) {
|
||||||
|
$_SESSION['user_roles'] = [];
|
||||||
|
}
|
||||||
|
return $_SESSION['user_roles'];
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasRole(string $role): bool
|
||||||
|
{
|
||||||
|
return in_array($role, currentUserRoles(), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Capacités connues — clé => label affiché dans l'admin
|
||||||
|
const KNOWN_CAPABILITIES = [
|
||||||
|
'view_sources_own' => 'Voir les sources de ses propres articles',
|
||||||
|
'view_sources_all' => 'Voir les sources de tous les articles',
|
||||||
|
'view_drafts_own' => 'Voir ses articles non publiés',
|
||||||
|
'view_drafts_all' => 'Voir tous les articles non publiés',
|
||||||
|
'edit_articles_own' => 'Modifier ses propres articles',
|
||||||
|
'edit_articles_all' => 'Modifier tous les articles',
|
||||||
|
'rate_articles' => 'Noter des articles',
|
||||||
|
];
|
||||||
|
|
||||||
|
// Groupes pour l'interface d'administration
|
||||||
|
// 'single' => pas de variante own/all
|
||||||
|
const CAPABILITY_GROUPS = [
|
||||||
|
['label' => 'Sources & métadonnées', 'own' => 'view_sources_own', 'all' => 'view_sources_all'],
|
||||||
|
['label' => 'Articles non publiés', 'own' => 'view_drafts_own', 'all' => 'view_drafts_all'],
|
||||||
|
['label' => 'Modification', 'own' => 'edit_articles_own', 'all' => 'edit_articles_all'],
|
||||||
|
['label' => 'Noter des articles', 'single' => 'rate_articles'],
|
||||||
|
];
|
||||||
|
|
||||||
|
function currentUserCapabilities(): array
|
||||||
|
{
|
||||||
|
if (!isLoggedIn()) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
if (isset($_SESSION['user_capabilities'])) {
|
||||||
|
return $_SESSION['user_capabilities'];
|
||||||
|
}
|
||||||
|
$pdo = dbPdo();
|
||||||
|
if (!$pdo) {
|
||||||
|
$_SESSION['user_capabilities'] = [];
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
$st = $pdo->prepare(
|
||||||
|
'SELECT DISTINCT rc.capability
|
||||||
|
FROM role_capabilities rc
|
||||||
|
JOIN user_roles ur ON ur.role_id = rc.role_id
|
||||||
|
WHERE ur.user_email = :e'
|
||||||
|
);
|
||||||
|
$st->execute([':e' => strtolower(currentUserEmail() ?? '')]);
|
||||||
|
$_SESSION['user_capabilities'] = $st->fetchAll(PDO::FETCH_COLUMN) ?: [];
|
||||||
|
} catch (\Throwable) {
|
||||||
|
$_SESSION['user_capabilities'] = [];
|
||||||
|
}
|
||||||
|
return $_SESSION['user_capabilities'];
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasCapability(string $cap): bool
|
||||||
|
{
|
||||||
|
if (isAdmin()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return in_array($cap, currentUserCapabilities(), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function canDoOnArticle(string $baseCap, array $article): bool
|
||||||
|
{
|
||||||
|
if (isAdmin()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (hasCapability($baseCap . '_all')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (hasCapability($baseCap . '_own')) {
|
||||||
|
$owner = strtolower($article['author'] ?? '');
|
||||||
|
return $owner !== '' && $owner === strtolower(currentUserEmail() ?? '');
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
function isAdmin(): bool
|
function isAdmin(): bool
|
||||||
{
|
{
|
||||||
$email = currentUserEmail();
|
$email = currentUserEmail();
|
||||||
if (!$email) {
|
if (!$email) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
// Fallback bootstrap : var d'env
|
||||||
$rawAdmin = $_ENV['ADMIN_EMAIL'] ?? (getenv('ADMIN_EMAIL') ?: '');
|
$rawAdmin = $_ENV['ADMIN_EMAIL'] ?? (getenv('ADMIN_EMAIL') ?: '');
|
||||||
$allowed = array_filter(array_map('trim', explode(',', (string)$rawAdmin)));
|
$allowed = array_filter(array_map('trim', explode(',', (string)$rawAdmin)));
|
||||||
return in_array(strtolower($email), array_map('strtolower', $allowed), true);
|
if (in_array(strtolower($email), array_map('strtolower', $allowed), true)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return hasRole('admin');
|
||||||
}
|
}
|
||||||
|
|
||||||
function ssoLogoutUrl(): string
|
function ssoLogoutUrl(): string
|
||||||
|
|||||||
@@ -9,3 +9,118 @@ function vd($var, ...$moreVars)
|
|||||||
$output = ob_get_clean();
|
$output = ob_get_clean();
|
||||||
echo "<pre>$output</pre>";
|
echo "<pre>$output</pre>";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Diff ligne-à-ligne via LCS. Retourne un tableau de [op, line] où
|
||||||
|
* op est '=' (inchangé), '-' (supprimé), '+' (ajouté).
|
||||||
|
*/
|
||||||
|
function lineDiff(string $old, string $new): array
|
||||||
|
{
|
||||||
|
$a = explode("\n", $old);
|
||||||
|
$b = explode("\n", $new);
|
||||||
|
$n = count($a);
|
||||||
|
$m = count($b);
|
||||||
|
|
||||||
|
if ($n * $m > 300000) {
|
||||||
|
return [['!', "Diff trop grand ({$n}×{$m} lignes), affichage brut."], ['-', $old], ['+', $new]];
|
||||||
|
}
|
||||||
|
|
||||||
|
$dp = array_fill(0, $n + 1, array_fill(0, $m + 1, 0));
|
||||||
|
for ($i = $n - 1; $i >= 0; $i--) {
|
||||||
|
for ($j = $m - 1; $j >= 0; $j--) {
|
||||||
|
$dp[$i][$j] = $a[$i] === $b[$j]
|
||||||
|
? 1 + $dp[$i + 1][$j + 1]
|
||||||
|
: max($dp[$i + 1][$j], $dp[$i][$j + 1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$diff = [];
|
||||||
|
$i = 0;
|
||||||
|
$j = 0;
|
||||||
|
while ($i < $n || $j < $m) {
|
||||||
|
if ($i < $n && $j < $m && $a[$i] === $b[$j]) {
|
||||||
|
$diff[] = ['=', $a[$i]];
|
||||||
|
$i++;
|
||||||
|
$j++;
|
||||||
|
} elseif ($j < $m && ($i >= $n || $dp[$i][$j + 1] >= $dp[$i + 1][$j])) {
|
||||||
|
$diff[] = ['+', $b[$j++]];
|
||||||
|
} else {
|
||||||
|
$diff[] = ['-', $a[$i++]];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $diff;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 16 couleurs RGB de base — distribuées sur le spectre, visuellement distinctes
|
||||||
|
const COLOR_PALETTE_16 = [
|
||||||
|
[220, 38, 38], // rouge
|
||||||
|
[234, 88, 12], // orange
|
||||||
|
[217, 119, 6], // ambre
|
||||||
|
[161, 142, 14], // jaune-olive
|
||||||
|
[77, 124, 15], // citron
|
||||||
|
[22, 163, 74], // vert
|
||||||
|
[4, 120, 87], // émeraude
|
||||||
|
[15, 118, 110], // sarcelle
|
||||||
|
[8, 145, 178], // cyan
|
||||||
|
[3, 105, 161], // ciel
|
||||||
|
[37, 99, 235], // bleu
|
||||||
|
[79, 70, 229], // indigo
|
||||||
|
[109, 40, 217], // violet
|
||||||
|
[147, 51, 234], // pourpre
|
||||||
|
[192, 38, 211], // fuchsia
|
||||||
|
[219, 39, 119], // rose
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Génère un dégradé CSS pour une catégorie.
|
||||||
|
* Avec $allCats, l'assignation est séquentielle (par ordre alpha) ;
|
||||||
|
* au-delà de 16, un décalage de teinte et d'angle différencie les palettes.
|
||||||
|
* Sans $allCats, fallback par hachage sur la palette.
|
||||||
|
*/
|
||||||
|
function coverGradient(string $seed, array $allCats = []): string
|
||||||
|
{
|
||||||
|
$key = strtolower(trim($seed));
|
||||||
|
|
||||||
|
if (!empty($allCats)) {
|
||||||
|
$keys = array_map(fn ($k) => strtolower(trim((string)$k)), array_keys($allCats));
|
||||||
|
$pos = array_search($key, $keys, true);
|
||||||
|
if ($pos !== false) {
|
||||||
|
$idx = (int) $pos;
|
||||||
|
$tier = (int) floor($idx / 16);
|
||||||
|
$ci = $idx % 16;
|
||||||
|
return _paletteGradient(COLOR_PALETTE_16[$ci], $tier);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hachage déterministe en l'absence de liste
|
||||||
|
$ci = abs(crc32($key)) % 16;
|
||||||
|
return _paletteGradient(COLOR_PALETTE_16[$ci], 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _paletteGradient(array $rgb, int $tier): string
|
||||||
|
{
|
||||||
|
[$r, $g, $b] = $rgb;
|
||||||
|
|
||||||
|
// Tier 0 : dégradé standard clair → foncé, 135°
|
||||||
|
// Tier 1 : plus saturé, angle inversé, 315°
|
||||||
|
// Tier 2+ : plus sombre encore, 225°
|
||||||
|
$tintMix = match ($tier) {
|
||||||
|
0 => 0.65, 1 => 0.48, default => 0.35
|
||||||
|
};
|
||||||
|
$shadeK = match ($tier) {
|
||||||
|
0 => 0.35, 1 => 0.25, default => 0.18
|
||||||
|
};
|
||||||
|
$angle = match ($tier) {
|
||||||
|
0 => 135, 1 => 315, default => 225
|
||||||
|
};
|
||||||
|
|
||||||
|
$tr = (int) round($r * (1 - $tintMix) + 255 * $tintMix);
|
||||||
|
$tg = (int) round($g * (1 - $tintMix) + 255 * $tintMix);
|
||||||
|
$tb = (int) round($b * (1 - $tintMix) + 255 * $tintMix);
|
||||||
|
|
||||||
|
$sr = (int) round($r * $shadeK);
|
||||||
|
$sg = (int) round($g * $shadeK);
|
||||||
|
$sb = (int) round($b * $shadeK);
|
||||||
|
|
||||||
|
return "linear-gradient({$angle}deg,rgb($tr,$tg,$tb) 0%,rgb($sr,$sg,$sb) 100%)";
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
<?php
|
||||||
|
ob_start();
|
||||||
|
$existingFiles = $articles->getFiles($addFilesArticle['uuid']);
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="d-flex align-items-center gap-3 mb-4">
|
||||||
|
<a href="/?action=edit&uuid=<?= rawurlencode($addFilesArticle['uuid']) ?>" class="btn btn-secondary btn-sm">← Retour</a>
|
||||||
|
<h1 class="h4 mb-0">Ajouter des fichiers</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="text-muted small mb-4">
|
||||||
|
Article : <strong><?= htmlspecialchars($addFilesArticle['title']) ?></strong>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="row g-4">
|
||||||
|
|
||||||
|
<!-- Formulaire d'upload -->
|
||||||
|
<div class="col-lg-5">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="POST"
|
||||||
|
action="/?action=add_files&uuid=<?= rawurlencode($addFilesArticle['uuid']) ?>"
|
||||||
|
enctype="multipart/form-data">
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="files" class="form-label fw-semibold">Fichiers à uploader</label>
|
||||||
|
<input type="file" class="form-control" id="files" name="files[]"
|
||||||
|
multiple required>
|
||||||
|
<div class="form-text">
|
||||||
|
Images → nommées <code>sha256-taille.ext</code><br>
|
||||||
|
Vidéos, PDF, autres → nom sanitisé
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<button type="submit" class="btn btn-primary">Uploader</button>
|
||||||
|
<a href="/?action=edit&uuid=<?= rawurlencode($addFilesArticle['uuid']) ?>"
|
||||||
|
class="btn btn-outline-secondary">Annuler</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Fichiers déjà présents -->
|
||||||
|
<?php if ($existingFiles): ?>
|
||||||
|
<div class="col-lg-7">
|
||||||
|
<h5 class="mb-3">Fichiers existants</h5>
|
||||||
|
<div class="list-group">
|
||||||
|
<?php foreach ($existingFiles as $f): ?>
|
||||||
|
<?php $fileUrl = '/file?uuid=' . rawurlencode($addFilesArticle['uuid']) . '&name=' . rawurlencode($f['name']); ?>
|
||||||
|
<div class="list-group-item d-flex align-items-center gap-3 py-2">
|
||||||
|
<?php if ($f['is_image']): ?>
|
||||||
|
<a href="<?= htmlspecialchars($fileUrl) ?>" target="_blank" rel="noopener" class="flex-shrink-0">
|
||||||
|
<img src="<?= htmlspecialchars($fileUrl) ?>" alt=""
|
||||||
|
style="width:48px;height:48px;object-fit:cover;border-radius:4px">
|
||||||
|
</a>
|
||||||
|
<?php else: ?>
|
||||||
|
<span style="width:48px;text-align:center;font-size:1.5rem;flex-shrink:0">
|
||||||
|
<?= match(true) {
|
||||||
|
str_starts_with($f['mime'], 'video/') => '🎬',
|
||||||
|
str_starts_with($f['mime'], 'audio/') => '🎵',
|
||||||
|
$f['mime'] === 'application/pdf' => '📑',
|
||||||
|
default => '📄',
|
||||||
|
} ?>
|
||||||
|
</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
<div class="flex-grow-1 overflow-hidden">
|
||||||
|
<code class="d-block small text-truncate"><?= htmlspecialchars($f['name']) ?></code>
|
||||||
|
<small class="text-muted"><?= number_format($f['size'] / 1024, 1) ?> Ko</small>
|
||||||
|
</div>
|
||||||
|
<?php if ($addFilesArticle['cover'] ?? '' === $f['name']): ?>
|
||||||
|
<span class="badge bg-primary flex-shrink-0">cover</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
$content = ob_get_clean();
|
||||||
|
$title = 'Ajouter des fichiers — ' . htmlspecialchars($addFilesArticle['title']);
|
||||||
|
include __DIR__ . '/layout.php';
|
||||||
@@ -0,0 +1,394 @@
|
|||||||
|
<?php
|
||||||
|
ob_start();
|
||||||
|
|
||||||
|
$now = time();
|
||||||
|
|
||||||
|
function adminStatusBadge(array $a, int $now): string
|
||||||
|
{
|
||||||
|
if (!$a['published']) {
|
||||||
|
return '<span class="badge bg-secondary">Brouillon</span>';
|
||||||
|
}
|
||||||
|
if (strtotime((string)($a['published_at'] ?? '')) > $now) {
|
||||||
|
return '<span class="badge bg-warning text-dark">Avant-première</span>';
|
||||||
|
}
|
||||||
|
return '<span class="badge bg-success">Publié</span>';
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h1 class="h3 mb-0">Administration</h1>
|
||||||
|
<a href="/?action=create" class="btn btn-primary btn-sm">+ Nouvel article</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Onglets -->
|
||||||
|
<ul class="nav nav-tabs mb-4">
|
||||||
|
<?php if (isAdmin()): ?>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link <?= $tab === 'dashboard' ? 'active' : '' ?>"
|
||||||
|
href="/?action=admin&tab=dashboard">Tableau de bord</a>
|
||||||
|
</li>
|
||||||
|
<?php endif; ?>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link <?= $tab === 'articles' ? 'active' : '' ?>"
|
||||||
|
href="/?action=admin&tab=articles"><?= isAdmin() ? 'Articles' : 'Mes articles' ?></a>
|
||||||
|
</li>
|
||||||
|
<?php if (isAdmin()): ?>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link <?= $tab === 'users' ? 'active' : '' ?>"
|
||||||
|
href="/?action=admin&tab=users">Utilisateurs</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link <?= $tab === 'roles' ? 'active' : '' ?>"
|
||||||
|
href="/?action=admin&tab=roles">Rôles</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="/?action=categories">Catégories</a>
|
||||||
|
</li>
|
||||||
|
<?php endif; ?>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<!-- ─────────────────────────── DASHBOARD ─────────────────────────── -->
|
||||||
|
<?php if ($tab === 'dashboard' && isAdmin()): ?>
|
||||||
|
|
||||||
|
<div class="row g-3 mb-4">
|
||||||
|
<?php
|
||||||
|
$stats = [
|
||||||
|
['label' => 'Publiés', 'value' => $adminData['published'], 'color' => 'success'],
|
||||||
|
['label' => 'Avant-premières', 'value' => $adminData['previews'], 'color' => 'warning'],
|
||||||
|
['label' => 'Brouillons', 'value' => $adminData['drafts'], 'color' => 'secondary'],
|
||||||
|
['label' => 'Total', 'value' => $adminData['total'], 'color' => 'primary'],
|
||||||
|
];
|
||||||
|
foreach ($stats as $s): ?>
|
||||||
|
<div class="col-6 col-md-3">
|
||||||
|
<div class="card text-center">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="display-6 fw-bold text-<?= $s['color'] ?>"><?= $s['value'] ?></div>
|
||||||
|
<div class="text-muted small"><?= $s['label'] ?></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h5>Activité récente</h5>
|
||||||
|
<table class="table table-sm table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Titre</th>
|
||||||
|
<th>Auteur</th>
|
||||||
|
<th>Statut</th>
|
||||||
|
<th>Modifié le</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ($adminData['recent'] as $a): ?>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<a href="/post/<?= htmlspecialchars($a['slug'] ?? '') ?>">
|
||||||
|
<?= htmlspecialchars($a['title']) ?>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td class="text-muted small"><?= htmlspecialchars($a['author'] ?? '–') ?></td>
|
||||||
|
<td><?= adminStatusBadge($a, $now) ?></td>
|
||||||
|
<td class="text-muted small">
|
||||||
|
<?= htmlspecialchars(date('d/m/Y H:i', strtotime((string)($a['updated_at'] ?? $a['created_at'] ?? '')))) ?>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<!-- ─────────────────────────── ARTICLES ─────────────────────────── -->
|
||||||
|
<?php elseif ($tab === 'articles'): ?>
|
||||||
|
|
||||||
|
<?php if (empty($adminData['articles'])): ?>
|
||||||
|
<p class="text-muted">Aucun article.</p>
|
||||||
|
<?php else: ?>
|
||||||
|
<table class="table table-sm table-hover align-middle">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Titre</th>
|
||||||
|
<?php if (isAdmin()): ?><th>Auteur</th><?php endif; ?>
|
||||||
|
<th>Catégorie</th>
|
||||||
|
<th>Statut</th>
|
||||||
|
<th>Date</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ($adminData['articles'] as $a): ?>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<a href="/post/<?= htmlspecialchars($a['slug'] ?? '') ?>">
|
||||||
|
<?= htmlspecialchars($a['title']) ?>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<?php if (isAdmin()): ?>
|
||||||
|
<td class="text-muted small"><?= htmlspecialchars($a['author'] ?? '–') ?></td>
|
||||||
|
<?php endif; ?>
|
||||||
|
<td class="text-muted small"><?= htmlspecialchars($a['category'] ?? '–') ?></td>
|
||||||
|
<td><?= adminStatusBadge($a, $now) ?></td>
|
||||||
|
<td class="text-muted small text-nowrap">
|
||||||
|
<?= htmlspecialchars(date('d/m/Y', strtotime((string)($a['published_at'] ?? $a['created_at'] ?? '')))) ?>
|
||||||
|
</td>
|
||||||
|
<td class="text-end text-nowrap">
|
||||||
|
<a href="/?action=edit&uuid=<?= htmlspecialchars($a['uuid']) ?>"
|
||||||
|
class="btn btn-outline-secondary btn-sm">Modifier</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<!-- ─────────────────────────── UTILISATEURS ─────────────────────────── -->
|
||||||
|
<?php elseif ($tab === 'users' && isAdmin()): ?>
|
||||||
|
|
||||||
|
<!-- Ajouter / attribuer un rôle -->
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header">Attribuer un rôle</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="post" action="/?action=admin_grant_role" class="row g-2 align-items-end">
|
||||||
|
<div class="col-md-5">
|
||||||
|
<label class="form-label small">Email</label>
|
||||||
|
<input type="email" name="email" class="form-control form-control-sm"
|
||||||
|
placeholder="utilisateur@exemple.fr" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label small">Rôle</label>
|
||||||
|
<select name="role" class="form-select form-select-sm" required>
|
||||||
|
<?php foreach ($adminData['roles'] as $r): ?>
|
||||||
|
<option value="<?= htmlspecialchars($r['name']) ?>">
|
||||||
|
<?= htmlspecialchars($r['label']) ?>
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<button type="submit" class="btn btn-primary btn-sm w-100">Attribuer</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Liste des utilisateurs -->
|
||||||
|
<?php if (empty($adminData['users'])): ?>
|
||||||
|
<p class="text-muted">Aucun utilisateur.</p>
|
||||||
|
<?php else: ?>
|
||||||
|
<table class="table table-sm table-hover align-middle">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Email</th>
|
||||||
|
<th>Statut</th>
|
||||||
|
<th>Rôles</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ($adminData['users'] as $u): ?>
|
||||||
|
<tr>
|
||||||
|
<td class="small"><?= htmlspecialchars($u['email']) ?></td>
|
||||||
|
<td>
|
||||||
|
<?php if ($u['is_active'] === null): ?>
|
||||||
|
<span class="badge bg-light text-muted">Pré-inscrit</span>
|
||||||
|
<?php elseif ($u['is_active']): ?>
|
||||||
|
<span class="badge bg-success">Actif</span>
|
||||||
|
<?php else: ?>
|
||||||
|
<span class="badge bg-danger">Inactif</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<?php foreach ($u['roles'] as $roleName): ?>
|
||||||
|
<span class="badge bg-primary me-1"><?= htmlspecialchars($roleName) ?></span>
|
||||||
|
<form method="post" action="/?action=admin_revoke_role"
|
||||||
|
class="d-inline"
|
||||||
|
data-confirm="Retirer le rôle <?= htmlspecialchars($roleName) ?> à <?= htmlspecialchars($u['email']) ?> ?">
|
||||||
|
<input type="hidden" name="email" value="<?= htmlspecialchars($u['email']) ?>">
|
||||||
|
<input type="hidden" name="role" value="<?= htmlspecialchars($roleName) ?>">
|
||||||
|
<button type="submit" class="btn btn-link btn-sm p-0 text-danger" title="Retirer">×</button>
|
||||||
|
</form>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php if (empty($u['roles'])): ?>
|
||||||
|
<span class="text-muted small">–</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
|
<td class="text-end">
|
||||||
|
<!-- Ajout rapide d'un rôle existant -->
|
||||||
|
<?php
|
||||||
|
$currentRoles = $u['roles'];
|
||||||
|
$missing = array_filter($adminData['roles'], fn ($r) => !in_array($r['name'], $currentRoles, true));
|
||||||
|
?>
|
||||||
|
<?php if ($missing): ?>
|
||||||
|
<form method="post" action="/?action=admin_grant_role" class="d-inline-flex gap-1">
|
||||||
|
<input type="hidden" name="email" value="<?= htmlspecialchars($u['email']) ?>">
|
||||||
|
<select name="role" class="form-select form-select-sm" style="width:auto">
|
||||||
|
<?php foreach ($missing as $r): ?>
|
||||||
|
<option value="<?= htmlspecialchars($r['name']) ?>">
|
||||||
|
<?= htmlspecialchars($r['label']) ?>
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
<button type="submit" class="btn btn-outline-primary btn-sm">+</button>
|
||||||
|
</form>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<!-- ─────────────────────────── RÔLES ─────────────────────────── -->
|
||||||
|
<?php elseif ($tab === 'roles' && isAdmin()): ?>
|
||||||
|
|
||||||
|
<div class="row g-4">
|
||||||
|
|
||||||
|
<!-- Liste des rôles existants -->
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<h5 class="mb-3">Rôles existants</h5>
|
||||||
|
<?php if (empty($adminData['roles'])): ?>
|
||||||
|
<p class="text-muted">Aucun rôle défini.</p>
|
||||||
|
<?php else: ?>
|
||||||
|
<table class="table table-sm table-hover align-middle">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style="width:160px">Nom technique</th>
|
||||||
|
<th>Label affiché</th>
|
||||||
|
<th class="text-center" style="width:90px">Utilisateurs</th>
|
||||||
|
<th style="width:100px"></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ($adminData['roles'] as $r): ?>
|
||||||
|
<tr>
|
||||||
|
<td><code class="text-body"><?= htmlspecialchars($r['name']) ?></code></td>
|
||||||
|
<td>
|
||||||
|
<form method="post" action="/?action=admin_update_role"
|
||||||
|
class="d-flex gap-2 align-items-center">
|
||||||
|
<input type="hidden" name="id" value="<?= (int)$r['id'] ?>">
|
||||||
|
<input type="text" name="label"
|
||||||
|
value="<?= htmlspecialchars($r['label']) ?>"
|
||||||
|
class="form-control form-control-sm" required>
|
||||||
|
<button type="submit" class="btn btn-outline-secondary btn-sm text-nowrap">
|
||||||
|
Sauver
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
<span class="badge bg-secondary"><?= (int)$r['user_count'] ?></span>
|
||||||
|
</td>
|
||||||
|
<td class="text-end">
|
||||||
|
<form method="post" action="/?action=admin_delete_role"
|
||||||
|
data-confirm="Supprimer le rôle «<?= htmlspecialchars($r['name']) ?>» ?<?= (int)$r['user_count'] > 0 ? ' ' . (int)$r['user_count'] . ' utilisateur(s) perdront ce rôle.' : '' ?>">
|
||||||
|
<input type="hidden" name="id" value="<?= (int)$r['id'] ?>">
|
||||||
|
<button type="submit" class="btn btn-outline-danger btn-sm">
|
||||||
|
Supprimer
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Créer un rôle -->
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">Nouveau rôle</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="post" action="/?action=admin_create_role">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label small fw-semibold">Nom technique</label>
|
||||||
|
<input type="text" name="name" class="form-control form-control-sm"
|
||||||
|
placeholder="ex : moderator"
|
||||||
|
pattern="[a-z0-9_-]+"
|
||||||
|
title="Lettres minuscules, chiffres, tirets et underscores uniquement"
|
||||||
|
required>
|
||||||
|
<div class="form-text">Utilisé dans le code — ne change pas.</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label small fw-semibold">Label affiché</label>
|
||||||
|
<input type="text" name="label" class="form-control form-control-sm"
|
||||||
|
placeholder="ex : Modérateur" required>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary btn-sm w-100">Créer</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Permissions par rôle -->
|
||||||
|
<?php if (!empty($adminData['roles'])): ?>
|
||||||
|
<div class="col-12 mt-2">
|
||||||
|
<h5 class="mb-3">Permissions par rôle</h5>
|
||||||
|
<p class="text-muted small mb-3">Le rôle <code>admin</code> a toutes les permissions implicitement.</p>
|
||||||
|
<div class="row g-3">
|
||||||
|
<?php foreach ($adminData['roles'] as $r):
|
||||||
|
if ($r['name'] === 'admin') {
|
||||||
|
continue;
|
||||||
|
} ?>
|
||||||
|
<div class="col-md-6 col-lg-4">
|
||||||
|
<div class="card h-100">
|
||||||
|
<div class="card-header py-2 d-flex align-items-center justify-content-between">
|
||||||
|
<span class="fw-semibold"><?= htmlspecialchars($r['label']) ?></span>
|
||||||
|
<code class="text-muted small"><?= htmlspecialchars($r['name']) ?></code>
|
||||||
|
</div>
|
||||||
|
<div class="card-body py-3">
|
||||||
|
<form method="post" action="/?action=admin_update_role_caps">
|
||||||
|
<input type="hidden" name="role_id" value="<?= (int)$r['id'] ?>">
|
||||||
|
<?php foreach (CAPABILITY_GROUPS as $group): ?>
|
||||||
|
<?php if (isset($group['single'])): ?>
|
||||||
|
<?php $cap = $group['single']; ?>
|
||||||
|
<div class="form-check mb-3">
|
||||||
|
<input class="form-check-input" type="checkbox"
|
||||||
|
name="caps[]" value="<?= htmlspecialchars($cap) ?>"
|
||||||
|
id="cap_<?= (int)$r['id'] ?>_<?= htmlspecialchars($cap) ?>"
|
||||||
|
<?= in_array($cap, $r['capabilities'], true) ? 'checked' : '' ?>>
|
||||||
|
<label class="form-check-label small fw-semibold"
|
||||||
|
for="cap_<?= (int)$r['id'] ?>_<?= htmlspecialchars($cap) ?>">
|
||||||
|
<?= htmlspecialchars($group['label']) ?>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="mb-3">
|
||||||
|
<div class="small fw-semibold mb-1"><?= htmlspecialchars($group['label']) ?></div>
|
||||||
|
<div class="d-flex gap-3 ps-1">
|
||||||
|
<?php foreach (['own' => 'Propres articles', 'all' => 'Tous'] as $scope => $scopeLabel):
|
||||||
|
$cap = $group[$scope]; ?>
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="checkbox"
|
||||||
|
name="caps[]" value="<?= htmlspecialchars($cap) ?>"
|
||||||
|
id="cap_<?= (int)$r['id'] ?>_<?= htmlspecialchars($cap) ?>"
|
||||||
|
<?= in_array($cap, $r['capabilities'], true) ? 'checked' : '' ?>>
|
||||||
|
<label class="form-check-label small"
|
||||||
|
for="cap_<?= (int)$r['id'] ?>_<?= htmlspecialchars($cap) ?>">
|
||||||
|
<?= $scopeLabel ?>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<button type="submit" class="btn btn-outline-secondary btn-sm mt-1 w-100">
|
||||||
|
Enregistrer
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
$content = ob_get_clean();
|
||||||
|
$title = 'Administration — varlog';
|
||||||
|
include __DIR__ . '/layout.php';
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
<?php ob_start(); ?>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h1 class="mb-0">Catégories</h1>
|
||||||
|
<a href="/" class="btn btn-secondary btn-sm">← Retour</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-4">
|
||||||
|
|
||||||
|
<!-- Liste des catégories existantes -->
|
||||||
|
<div class="col-lg-9">
|
||||||
|
<h5 class="mb-3">Catégories existantes</h5>
|
||||||
|
|
||||||
|
<?php if (empty($cats)): ?>
|
||||||
|
<p class="text-muted">Aucune catégorie définie.</p>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="d-flex flex-column gap-2">
|
||||||
|
<?php foreach ($cats as $cat => $count):
|
||||||
|
$gradient = coverGradient($cat, $cats); ?>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body py-2 px-3 d-flex align-items-center gap-3">
|
||||||
|
|
||||||
|
<!-- Swatch -->
|
||||||
|
<div style="width:40px;height:40px;border-radius:8px;flex-shrink:0;background:<?= htmlspecialchars($gradient) ?>"></div>
|
||||||
|
|
||||||
|
<!-- Nom + count -->
|
||||||
|
<div style="min-width:140px">
|
||||||
|
<strong><?= htmlspecialchars($cat) ?></strong>
|
||||||
|
<small class="text-muted ms-2"><?= $count ?> article<?= $count > 1 ? 's' : '' ?></small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Renommer -->
|
||||||
|
<form method="POST" action="/?action=rename_category"
|
||||||
|
class="d-flex align-items-center gap-2 flex-grow-1"
|
||||||
|
data-confirm="Renommer « <?= htmlspecialchars($cat) ?> » ?">
|
||||||
|
<input type="hidden" name="old" value="<?= htmlspecialchars($cat) ?>">
|
||||||
|
<input type="text" name="new" class="form-control form-control-sm"
|
||||||
|
placeholder="Nouveau nom" required>
|
||||||
|
<button type="submit" class="btn btn-outline-primary btn-sm text-nowrap">Renommer</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- Privée -->
|
||||||
|
<?php $isPriv = in_array($cat, $privateCats, true); ?>
|
||||||
|
<form method="POST" action="/?action=toggle_private_category">
|
||||||
|
<input type="hidden" name="category" value="<?= htmlspecialchars($cat) ?>">
|
||||||
|
<button type="submit"
|
||||||
|
class="btn btn-sm text-nowrap <?= $isPriv ? 'btn-secondary' : 'btn-outline-secondary' ?>">
|
||||||
|
🔒 <?= $isPriv ? 'Privée' : 'Publique' ?>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- Supprimer -->
|
||||||
|
<form method="POST" action="/?action=delete_category"
|
||||||
|
data-confirm="Retirer la catégorie « <?= htmlspecialchars($cat) ?> » de tous les articles ?">
|
||||||
|
<input type="hidden" name="category" value="<?= htmlspecialchars($cat) ?>">
|
||||||
|
<button type="submit" class="btn btn-outline-danger btn-sm">Supprimer</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Palette & Nouvelle catégorie -->
|
||||||
|
<div class="col-lg-3">
|
||||||
|
<h5 class="mb-3">Nouvelle catégorie</h5>
|
||||||
|
<div class="card mb-3">
|
||||||
|
<div class="card-body">
|
||||||
|
<p class="text-muted small mb-3">
|
||||||
|
Créez une catégorie en l'assignant à un article.
|
||||||
|
La prochaine reçoit la couleur n°<?= count($cats) % 16 + 1 ?>.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- Palette des 16 couleurs -->
|
||||||
|
<div class="d-flex flex-wrap gap-2">
|
||||||
|
<?php
|
||||||
|
$nextIdx = count($cats) % 16;
|
||||||
|
foreach (COLOR_PALETTE_16 as $i => $rgb):
|
||||||
|
$g = _paletteGradient($rgb, 0);
|
||||||
|
$active = $i === $nextIdx;
|
||||||
|
?>
|
||||||
|
<div title="Couleur <?= $i + 1 ?>"
|
||||||
|
style="width:28px;height:28px;border-radius:6px;background:<?= htmlspecialchars($g) ?>;
|
||||||
|
<?= $active ? 'outline:2px solid #0d6efd;outline-offset:2px' : 'opacity:.75' ?>">
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
$content = ob_get_clean();
|
||||||
|
$title = 'Catégories';
|
||||||
|
include __DIR__ . '/layout.php';
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
<?php ob_start(); ?>
|
||||||
|
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-lg-8 col-xl-7">
|
||||||
|
|
||||||
|
<div class="d-flex align-items-center gap-3 mb-4">
|
||||||
|
<a href="/?action=import_image&uuid=<?= rawurlencode($ackArticle['uuid']) ?>"
|
||||||
|
class="btn btn-secondary btn-sm">← Retour</a>
|
||||||
|
<h1 class="h4 mb-0">Confirmation — droits d'auteur</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="alert alert-warning d-flex gap-2 mb-4">
|
||||||
|
<span style="font-size:1.3rem;flex-shrink:0">⚠</span>
|
||||||
|
<div>
|
||||||
|
<strong>Vous êtes sur le point de copier ce fichier sur votre serveur :</strong>
|
||||||
|
<code class="d-block mt-1 text-break small"><?= htmlspecialchars($ackUrl) ?></code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Contexte légal -->
|
||||||
|
<div class="card mb-3">
|
||||||
|
<div class="card-header fw-semibold">Ce que dit la loi française</div>
|
||||||
|
<div class="card-body small lh-base">
|
||||||
|
<p>En France, <strong>toute œuvre de l'esprit est protégée dès sa création</strong> sans
|
||||||
|
formalité d'enregistrement (art. L.111-1 CPI). Cela inclut les photographies, illustrations,
|
||||||
|
textes, vidéos et musiques.</p>
|
||||||
|
|
||||||
|
<p>Reproduire ou diffuser publiquement une œuvre sans l'autorisation de son auteur constitue
|
||||||
|
une <strong>contrefaçon</strong> (art. L.335-2 CPI), passible de :</p>
|
||||||
|
<ul class="mb-2">
|
||||||
|
<li><strong>3 ans d'emprisonnement</strong></li>
|
||||||
|
<li><strong>300 000 € d'amende</strong></li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<p class="mb-0">L'exception d'<em>usage privé</em> (art. L.122-5 1° CPI) est strictement
|
||||||
|
personnelle et <strong>ne couvre pas la publication sur un blog</strong>, même non commercial
|
||||||
|
et même à audience restreinte.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Cas autorisés -->
|
||||||
|
<div class="card mb-3 border-success">
|
||||||
|
<div class="card-header fw-semibold text-success-emphasis bg-success-subtle">
|
||||||
|
✓ Cas où vous pouvez légalement télécharger ce fichier
|
||||||
|
</div>
|
||||||
|
<ul class="list-group list-group-flush small">
|
||||||
|
<li class="list-group-item">
|
||||||
|
<strong>Vous êtes l'auteur</strong> ou co-auteur du fichier
|
||||||
|
</li>
|
||||||
|
<li class="list-group-item">
|
||||||
|
Le fichier est distribué sous une <strong>licence libre compatible</strong> avec la
|
||||||
|
reproduction publique : <span class="font-monospace">CC0</span>,
|
||||||
|
<span class="font-monospace">CC BY</span>,
|
||||||
|
<span class="font-monospace">CC BY-SA</span>,
|
||||||
|
<span class="font-monospace">CC BY-ND</span>,
|
||||||
|
domaine public, etc.<br>
|
||||||
|
<small class="text-danger">⚠ CC BY-NC ne suffit pas si le blog génère des revenus,
|
||||||
|
même indirects.</small>
|
||||||
|
</li>
|
||||||
|
<li class="list-group-item">
|
||||||
|
Vous disposez d'une <strong>autorisation écrite explicite</strong> de l'auteur ou
|
||||||
|
du titulaire des droits patrimoniaux
|
||||||
|
</li>
|
||||||
|
<li class="list-group-item">
|
||||||
|
L'œuvre est dans le <strong>domaine public</strong> : 70 ans révolus après le décès
|
||||||
|
de l'auteur en Union Européenne (art. L.123-1 CPI)
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Formulaire de confirmation -->
|
||||||
|
<form method="POST"
|
||||||
|
action="/?action=add_file_from_url&uuid=<?= rawurlencode($ackArticle['uuid']) ?>">
|
||||||
|
<input type="hidden" name="image_url" value="<?= htmlspecialchars($ackUrl) ?>">
|
||||||
|
<input type="hidden" name="img_title" value="<?= htmlspecialchars($ackTitle) ?>">
|
||||||
|
<input type="hidden" name="img_author" value="<?= htmlspecialchars($ackAuthor) ?>">
|
||||||
|
<input type="hidden" name="img_source" value="<?= htmlspecialchars($ackSource) ?>">
|
||||||
|
<input type="hidden" name="meta_json" value="<?= htmlspecialchars($ackMetaJson) ?>">
|
||||||
|
<input type="hidden" name="mode" value="download">
|
||||||
|
<input type="hidden" name="copyright_acked" value="1">
|
||||||
|
<?php if ($ackIsCover): ?>
|
||||||
|
<input type="hidden" name="is_cover" value="1">
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div class="card border-primary mb-4">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="checkbox"
|
||||||
|
id="ack_check" name="ack_check" required>
|
||||||
|
<label class="form-check-label" for="ack_check">
|
||||||
|
<strong>Je certifie disposer des droits nécessaires</strong> pour reproduire
|
||||||
|
et publier ce fichier sur ce blog, conformément au Code de la Propriété
|
||||||
|
Intellectuelle. Je comprends que cette déclaration engage ma responsabilité
|
||||||
|
personnelle en cas de contentieux.
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<button type="submit" class="btn btn-primary">Télécharger et insérer</button>
|
||||||
|
<a href="/?action=import_image&uuid=<?= rawurlencode($ackArticle['uuid']) ?>"
|
||||||
|
class="btn btn-outline-secondary">Annuler</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
$content = ob_get_clean();
|
||||||
|
$title = 'Droits d\'auteur — confirmation';
|
||||||
|
include __DIR__ . '/layout.php';
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
<?php
|
||||||
|
ob_start();
|
||||||
|
$revMeta = $revisions[$revIndex] ?? [];
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="d-flex align-items-center gap-3 mb-3 flex-wrap">
|
||||||
|
<a href="/?action=edit&uuid=<?= htmlspecialchars($article['uuid']) ?>" class="btn btn-secondary btn-sm">← Retour</a>
|
||||||
|
<div>
|
||||||
|
<strong><?= htmlspecialchars($article['title']) ?></strong>
|
||||||
|
— révision #<?= (int)($revMeta['n'] ?? $revIndex + 1) ?>
|
||||||
|
<span class="text-muted small">
|
||||||
|
du <?= htmlspecialchars(date('d/m/Y H:i', strtotime((string)($revMeta['date'] ?? '')))) ?>
|
||||||
|
<?= !empty($revMeta['comment']) ? '— ' . htmlspecialchars($revMeta['comment']) : '' ?>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex gap-4 mb-2 small">
|
||||||
|
<span class="diff-del px-2 py-1 rounded">− Supprimé</span>
|
||||||
|
<span class="diff-ins px-2 py-1 rounded">+ Ajouté</span>
|
||||||
|
<span class="diff-eq px-2 py-1 rounded text-muted">= Inchangé</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ($diffLines === []): ?>
|
||||||
|
<div class="alert alert-success">Aucune différence — le contenu est identique.</div>
|
||||||
|
<?php else: ?>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
// Groupe les lignes : affiche contexte de 3 lignes autour des changements
|
||||||
|
$CONTEXT = 3;
|
||||||
|
$total = count($diffLines);
|
||||||
|
$show = [];
|
||||||
|
for ($i = 0; $i < $total; $i++) {
|
||||||
|
if ($diffLines[$i][0] !== '=') {
|
||||||
|
for ($c = max(0, $i - $CONTEXT); $c <= min($total - 1, $i + $CONTEXT); $c++) {
|
||||||
|
$show[$c] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$inEllipsis = false;
|
||||||
|
?>
|
||||||
|
<div class="diff-view font-monospace small">
|
||||||
|
<?php for ($i = 0; $i < $total; $i++): ?>
|
||||||
|
<?php [$op, $line] = $diffLines[$i]; ?>
|
||||||
|
<?php if (!isset($show[$i])): ?>
|
||||||
|
<?php if (!$inEllipsis): $inEllipsis = true; ?>
|
||||||
|
<div class="diff-ellipsis text-muted px-2">⋯</div>
|
||||||
|
<?php endif;
|
||||||
|
continue; ?>
|
||||||
|
<?php else: $inEllipsis = false; endif; ?>
|
||||||
|
<?php if ($op === '!'): ?>
|
||||||
|
<div class="diff-warning text-warning px-2"><?= htmlspecialchars($line) ?></div>
|
||||||
|
<?php elseif ($op === '-'): ?>
|
||||||
|
<div class="diff-del px-2">− <?= htmlspecialchars($line) ?></div>
|
||||||
|
<?php elseif ($op === '+'): ?>
|
||||||
|
<div class="diff-ins px-2">+ <?= htmlspecialchars($line) ?></div>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="diff-eq px-2 text-muted"> <?= htmlspecialchars($line) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php endfor; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.diff-view { border: 1px solid var(--bs-border-color, #dee2e6); border-radius: 6px; overflow-x: auto; }
|
||||||
|
.diff-view > div { padding: 1px 8px; white-space: pre; line-height: 1.5; }
|
||||||
|
.diff-del { background: #ffeef0; color: #b91c1c; }
|
||||||
|
.diff-ins { background: #e6ffec; color: #15803d; }
|
||||||
|
.diff-eq { }
|
||||||
|
.diff-ellipsis { background: #f8f9fa; padding: 2px 8px; user-select: none; }
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
$content = ob_get_clean();
|
||||||
|
$title = 'Diff — ' . htmlspecialchars($article['title']);
|
||||||
|
include __DIR__ . '/layout.php';
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
<?php ob_start(); ?>
|
||||||
|
|
||||||
|
<div class="d-flex align-items-center gap-3 mb-4">
|
||||||
|
<a href="/?action=edit&uuid=<?= rawurlencode($importArticle['uuid']) ?>" class="btn btn-secondary btn-sm">← Retour</a>
|
||||||
|
<h1 class="h4 mb-0">Importer un fichier depuis une URL</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="text-muted small mb-4">
|
||||||
|
Article : <strong><?= htmlspecialchars($importArticle['title']) ?></strong>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<?php if ($importError): ?>
|
||||||
|
<div class="alert alert-warning">
|
||||||
|
URL invalide ou inaccessible — vérifiez que le lien est correct et que le serveur peut y accéder.
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div class="card" style="max-width:640px">
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="POST" action="/?action=import_image_step2&uuid=<?= rawurlencode($importArticle['uuid']) ?>">
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="form-label fw-semibold">URL du fichier <span class="text-danger">*</span></label>
|
||||||
|
<input type="url" name="image_url" class="form-control font-monospace"
|
||||||
|
placeholder="https://…/document.pdf"
|
||||||
|
value="<?= htmlspecialchars($_GET['image_url'] ?? '') ?>"
|
||||||
|
required autofocus>
|
||||||
|
<div class="form-text">Les métadonnées seront récupérées automatiquement à l'étape suivante.</div>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<button type="submit" class="btn btn-primary">Suivant →</button>
|
||||||
|
<a href="/?action=edit&uuid=<?= rawurlencode($importArticle['uuid']) ?>"
|
||||||
|
class="btn btn-outline-secondary">Annuler</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
$content = ob_get_clean();
|
||||||
|
$title = 'Importer un fichier — ' . htmlspecialchars($importArticle['title']);
|
||||||
|
include __DIR__ . '/layout.php';
|
||||||
@@ -0,0 +1,205 @@
|
|||||||
|
<?php
|
||||||
|
ob_start();
|
||||||
|
|
||||||
|
$isPdf = ($step2Meta['mime'] ?? '') === 'application/pdf';
|
||||||
|
$isHtml = str_starts_with($step2Meta['mime'] ?? '', 'text/html');
|
||||||
|
|
||||||
|
// Ordre + labels contextuels
|
||||||
|
$metaRows = [
|
||||||
|
'mime' => 'Type',
|
||||||
|
'size' => 'Taille',
|
||||||
|
// PDF
|
||||||
|
'pages' => 'Pages',
|
||||||
|
'page_size' => 'Format',
|
||||||
|
'pdf_version' => 'Version PDF',
|
||||||
|
// Image
|
||||||
|
'width' => 'Dimensions',
|
||||||
|
'camera' => 'Appareil',
|
||||||
|
// HTML
|
||||||
|
'site_name' => 'Site',
|
||||||
|
'og_type' => 'Type',
|
||||||
|
'language' => 'Langue',
|
||||||
|
// Commun
|
||||||
|
'author' => $isHtml ? 'Auteur' : ($isPdf ? 'Auteur' : 'Auteur EXIF'),
|
||||||
|
'date' => $isPdf ? 'Créé le' : ($isHtml ? 'Publié le' : 'Prise de vue'),
|
||||||
|
'description' => 'Description',
|
||||||
|
'subject' => 'Sujet',
|
||||||
|
'keywords' => 'Mots-clés',
|
||||||
|
'copyright' => 'Copyright',
|
||||||
|
// PDF logiciel
|
||||||
|
'creator' => 'Créé avec',
|
||||||
|
'producer' => 'Produit par',
|
||||||
|
// HTML liens
|
||||||
|
'canonical' => 'URL canonique',
|
||||||
|
'og_image' => 'Image OG',
|
||||||
|
];
|
||||||
|
|
||||||
|
$hasTitle = !empty($step2Meta['title']);
|
||||||
|
$preAuthor = $step2Meta['author'] ?? $step2Meta['credit'] ?? '';
|
||||||
|
$preSource = $step2Meta['canonical'] ?? $step2Meta['source'] ?? $step2Url;
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="d-flex align-items-center gap-3 mb-4">
|
||||||
|
<a href="/?action=import_image&uuid=<?= rawurlencode($step2Article['uuid']) ?>" class="btn btn-secondary btn-sm">← Retour</a>
|
||||||
|
<h1 class="h4 mb-0">Importer un fichier</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="text-muted small mb-4">
|
||||||
|
Article : <strong><?= htmlspecialchars($step2Article['title']) ?></strong>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<?php if ($step2Screenshot ?? null): ?>
|
||||||
|
<!-- Aperçu screenshot -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<p class="fw-semibold small mb-2">Aperçu de la page</p>
|
||||||
|
<?php
|
||||||
|
$previewMtime = @filemtime(BASE_PATH . '/data/' . $step2Article['uuid'] . '/files/' . $step2Screenshot) ?: time();
|
||||||
|
?>
|
||||||
|
<img src="/file?uuid=<?= rawurlencode($step2Article['uuid']) ?>&name=<?= rawurlencode($step2Screenshot) ?>&v=<?= $previewMtime ?>"
|
||||||
|
class="img-fluid rounded shadow-sm d-block"
|
||||||
|
style="max-height:320px;object-fit:cover;object-position:top"
|
||||||
|
alt="Aperçu">
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<!-- Résumé métadonnées -->
|
||||||
|
<?php
|
||||||
|
$visibleRows = array_filter($metaRows, fn ($label, $key) => !empty($step2Meta[$key]), ARRAY_FILTER_USE_BOTH);
|
||||||
|
if ($visibleRows): ?>
|
||||||
|
<div class="card mb-4" style="max-width:640px">
|
||||||
|
<div class="card-header small fw-semibold">Métadonnées du fichier</div>
|
||||||
|
<div class="card-body p-0">
|
||||||
|
<table class="table table-sm table-borderless mb-0 small">
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ($visibleRows as $key => $label): ?>
|
||||||
|
<?php
|
||||||
|
$val = $step2Meta[$key];
|
||||||
|
$cellHtml = match($key) {
|
||||||
|
'size' => htmlspecialchars(round($val / 1024) . ' Ko'),
|
||||||
|
'width' => htmlspecialchars($val . ' × ' . ($step2Meta['height'] ?? '?') . ' px'),
|
||||||
|
'og_image' => '<img src="' . htmlspecialchars((string)$val) . '" style="max-height:72px;max-width:200px;border-radius:4px" alt="">',
|
||||||
|
'canonical' => '<a href="' . htmlspecialchars((string)$val) . '" target="_blank" rel="noopener" class="small text-break">' . htmlspecialchars((string)$val) . '</a>',
|
||||||
|
default => htmlspecialchars((string)$val),
|
||||||
|
};
|
||||||
|
?>
|
||||||
|
<tr>
|
||||||
|
<th class="text-muted fw-normal ps-3 pe-3 text-nowrap align-top" style="width:130px"><?= $label ?></th>
|
||||||
|
<td class="pe-3"><?= $cellHtml ?></td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<!-- Formulaire -->
|
||||||
|
<div class="card" style="max-width:640px">
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="POST" action="/?action=add_file_from_url&uuid=<?= rawurlencode($step2Article['uuid']) ?>">
|
||||||
|
<input type="hidden" name="image_url" value="<?= htmlspecialchars($step2Url) ?>">
|
||||||
|
<?php if ($step2Screenshot ?? null): ?>
|
||||||
|
<input type="hidden" name="screenshot_file" value="<?= htmlspecialchars($step2Screenshot) ?>">
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php
|
||||||
|
$metaToStore = array_filter(
|
||||||
|
array_diff_key($step2Meta, array_flip(['ok', 'height'])),
|
||||||
|
fn ($v) => $v !== null && $v !== ''
|
||||||
|
);
|
||||||
|
?>
|
||||||
|
<input type="hidden" name="meta_json"
|
||||||
|
value="<?= htmlspecialchars(json_encode($metaToStore, JSON_UNESCAPED_UNICODE)) ?>">
|
||||||
|
|
||||||
|
<!-- Titre (obligatoire) -->
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label fw-semibold">
|
||||||
|
Titre <span class="text-danger">*</span>
|
||||||
|
</label>
|
||||||
|
<input type="text" name="img_title" class="form-control"
|
||||||
|
placeholder="ex. Compte rendu du conseil municipal"
|
||||||
|
value="<?= htmlspecialchars($step2Meta['title'] ?? '') ?>"
|
||||||
|
required autofocus>
|
||||||
|
<?php if (!$hasTitle): ?>
|
||||||
|
<div class="form-text text-warning small">
|
||||||
|
Titre non trouvé dans les métadonnées — saisie requise.
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Mode -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<p class="form-label fw-semibold mb-2">Mode</p>
|
||||||
|
<div class="form-check mb-1">
|
||||||
|
<input class="form-check-input" type="radio" name="mode" id="mode_link" value="link" checked>
|
||||||
|
<label class="form-check-label" for="mode_link">
|
||||||
|
<strong>Lien externe</strong>
|
||||||
|
<span class="text-muted small"> — insère une référence, le fichier reste chez l'hôte</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="radio" name="mode" id="mode_download" value="download">
|
||||||
|
<label class="form-check-label" for="mode_download">
|
||||||
|
<strong>Télécharger sur le serveur</strong>
|
||||||
|
<span class="text-muted small"> — copie locale du fichier</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<?php if ($step2Screenshot ?? null): ?>
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="radio" name="mode" id="mode_screenshot" value="screenshot">
|
||||||
|
<label class="form-check-label" for="mode_screenshot">
|
||||||
|
<strong>Enregistrer la capture d'écran</strong>
|
||||||
|
<span class="text-muted small"> — sauvegarde l'aperçu comme image</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<div id="copyright-warning" class="alert alert-warning mt-3 mb-0 small" style="display:none">
|
||||||
|
<strong>Droits d'auteur.</strong> Une page de confirmation légale vous sera
|
||||||
|
présentée avant le téléchargement.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Auteur / source : toujours visibles pour les pages web, download-only sinon -->
|
||||||
|
<div id="download-fields" <?= $isHtml ? '' : 'style="display:none"' ?>>
|
||||||
|
<?php if ($preAuthor || $isHtml): ?>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label fw-semibold">Auteur / crédit</label>
|
||||||
|
<input type="text" name="img_author" class="form-control"
|
||||||
|
placeholder="ex. Jane Doe"
|
||||||
|
value="<?= htmlspecialchars($preAuthor) ?>">
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label fw-semibold">URL source</label>
|
||||||
|
<input type="url" name="img_source" class="form-control font-monospace"
|
||||||
|
placeholder="https://…"
|
||||||
|
value="<?= htmlspecialchars($preSource) ?>">
|
||||||
|
<div class="form-text">Laissé vide → URL du fichier utilisée comme source.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if (!$isHtml || ($step2Screenshot ?? null)): ?>
|
||||||
|
<div class="mb-4">
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="checkbox" name="is_cover" id="is_cover">
|
||||||
|
<label class="form-check-label" for="is_cover">
|
||||||
|
Définir comme image de couverture
|
||||||
|
<span class="text-muted small">(images uniquement, sera nommée <code>cover.jpg</code>)</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<button type="submit" class="btn btn-primary">Valider</button>
|
||||||
|
<a href="/?action=edit&uuid=<?= rawurlencode($step2Article['uuid']) ?>"
|
||||||
|
class="btn btn-outline-secondary">Annuler</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
$content = ob_get_clean();
|
||||||
|
$title = 'Importer un fichier — ' . htmlspecialchars($step2Article['title']);
|
||||||
|
include __DIR__ . '/layout.php';
|
||||||
@@ -49,17 +49,54 @@
|
|||||||
<span class="navbar-toggler-icon"></span>
|
<span class="navbar-toggler-icon"></span>
|
||||||
</button>
|
</button>
|
||||||
<div class="collapse navbar-collapse" id="navbarContent">
|
<div class="collapse navbar-collapse" id="navbarContent">
|
||||||
<ul class="navbar-nav ms-auto">
|
<?php
|
||||||
<?php if (function_exists('isAdmin') && isAdmin()): ?>
|
$_layoutAction = $_GET['action'] ?? 'list';
|
||||||
<li class="nav-item"><a class="nav-link" href="/?action=create">Nouveau post</a></li>
|
if (($_layoutAction === 'list' || $_layoutAction === '') && isset($articles)):
|
||||||
|
$_layoutPrivateCats = $articles->getPrivateCategories();
|
||||||
|
$_layoutCats = array_filter(
|
||||||
|
$articles->getCategories(),
|
||||||
|
function ($cat) use ($_layoutPrivateCats) {
|
||||||
|
return isLoggedIn() || !in_array($cat, $_layoutPrivateCats, true);
|
||||||
|
},
|
||||||
|
ARRAY_FILTER_USE_KEY
|
||||||
|
);
|
||||||
|
$_layoutCurrentCat = trim($_GET['cat'] ?? '');
|
||||||
|
if (!empty($_layoutCats)):
|
||||||
|
?>
|
||||||
|
<ul class="navbar-nav me-auto navbar-cats flex-nowrap overflow-auto gap-1">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link nav-cat <?= $_layoutCurrentCat === '' ? 'active' : '' ?>" href="/">Tous</a>
|
||||||
|
</li>
|
||||||
|
<?php foreach ($_layoutCats as $catName => $catCount):
|
||||||
|
$isPriv = in_array($catName, $_layoutPrivateCats, true); ?>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link nav-cat <?= $_layoutCurrentCat === $catName ? 'active' : '' ?>"
|
||||||
|
href="/?cat=<?= rawurlencode($catName) ?>">
|
||||||
|
<?= htmlspecialchars($catName) ?>
|
||||||
|
<?php if ($isPriv): ?><span class="ms-1" style="font-size:.65em;opacity:.6">🔒</span><?php endif; ?>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</ul>
|
||||||
|
<?php else: ?>
|
||||||
|
<ul class="navbar-nav me-auto"></ul>
|
||||||
|
<?php endif;
|
||||||
|
else: ?>
|
||||||
|
<ul class="navbar-nav me-auto"></ul>
|
||||||
|
<?php endif; ?>
|
||||||
|
<ul class="navbar-nav">
|
||||||
|
<?php if (function_exists('isLoggedIn') && isLoggedIn()): ?>
|
||||||
|
<li class="nav-item"><a class="nav-link" href="/?action=admin">Admin</a></li>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<?php if (function_exists('isLoggedIn') && isLoggedIn()): ?>
|
<?php if (function_exists('isLoggedIn') && isLoggedIn()): ?>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="/logout.php" title="Déconnexion">
|
<a class="nav-link" href="/?action=profile">
|
||||||
<?= htmlspecialchars(currentUserEmail() ?? '') ?>
|
<?= htmlspecialchars(function_exists('currentUserName') ? currentUserName() : (currentUserEmail() ?? '')) ?>
|
||||||
<small class="text-muted">(déconnexion)</small>
|
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link text-muted" href="/logout.php" title="Déconnexion">Déconnexion</a>
|
||||||
|
</li>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<li class="nav-item"><a class="nav-link" href="/login">Connexion</a></li>
|
<li class="nav-item"><a class="nav-link" href="/login">Connexion</a></li>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
@@ -69,7 +106,7 @@
|
|||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main class="container" role="main">
|
<main class="<?= htmlspecialchars($mainClass ?? 'container') ?>" role="main">
|
||||||
<?= $content ?>
|
<?= $content ?>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,15 @@ $dateValue = isset($published_at)
|
|||||||
: date('Y-m-d\TH:i');
|
: date('Y-m-d\TH:i');
|
||||||
?>
|
?>
|
||||||
|
|
||||||
|
<?php if ($action === 'edit'): ?>
|
||||||
|
<div id="vl-page"
|
||||||
|
data-uuid="<?= htmlspecialchars($uuid) ?>"
|
||||||
|
data-insert-url="<?= htmlspecialchars($insertUrl ?? '') ?>"
|
||||||
|
hidden></div>
|
||||||
|
<div class="row g-4">
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
<h1 class="mb-4"><?= $action === 'edit' ? 'Modifier l\'article' : 'Nouvel article' ?></h1>
|
<h1 class="mb-4"><?= $action === 'edit' ? 'Modifier l\'article' : 'Nouvel article' ?></h1>
|
||||||
|
|
||||||
<?php if (!empty($errors)): ?>
|
<?php if (!empty($errors)): ?>
|
||||||
@@ -20,12 +29,11 @@ $dateValue = isset($published_at)
|
|||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<form method="POST" action="<?= htmlspecialchars($formAction) ?>" enctype="multipart/form-data">
|
<form method="POST" action="<?= htmlspecialchars($formAction) ?>"<?= $action === 'create' ? ' enctype="multipart/form-data"' : '' ?>>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="title" class="form-label">Titre</label>
|
<label for="title" class="form-label">Titre</label>
|
||||||
<input type="text" class="form-control" id="title" name="title" required
|
<input type="text" class="form-control" id="title" name="title" required
|
||||||
value="<?= htmlspecialchars($title ?? '') ?>"
|
value="<?= htmlspecialchars($title ?? '') ?>">
|
||||||
oninput="autoSlug(this.value)">
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
@@ -34,10 +42,24 @@ $dateValue = isset($published_at)
|
|||||||
</label>
|
</label>
|
||||||
<input type="text" class="form-control form-control-sm font-monospace" id="slug" name="slug"
|
<input type="text" class="form-control form-control-sm font-monospace" id="slug" name="slug"
|
||||||
value="<?= htmlspecialchars($postSlug ?? '') ?>"
|
value="<?= htmlspecialchars($postSlug ?? '') ?>"
|
||||||
pattern="[a-z0-9][a-z0-9-]*"
|
pattern="[a-z0-9][a-z0-9\-]*"
|
||||||
placeholder="généré automatiquement depuis le titre">
|
placeholder="généré automatiquement depuis le titre">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="category" class="form-label">Catégorie</label>
|
||||||
|
<div class="d-flex align-items-center gap-2">
|
||||||
|
<input type="text" class="form-control form-control-sm" id="category" name="category"
|
||||||
|
value="<?= htmlspecialchars($category ?? '') ?>"
|
||||||
|
placeholder="ex : informatique, loisirs, photo…"
|
||||||
|
autocomplete="off">
|
||||||
|
<div id="cat-swatch" title="" style="width:40px;height:28px;border-radius:6px;flex-shrink:0;background:#e5e7eb;transition:background .25s"></div>
|
||||||
|
</div>
|
||||||
|
<small id="cat-hint" class="text-muted d-block mt-1"></small>
|
||||||
|
<div id="cat-free-swatches" class="d-flex flex-wrap gap-1 mt-2"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
<small class="text-muted">
|
<small class="text-muted">
|
||||||
Écris en <strong>Markdown</strong> — les fichiers uploadés sont référençables dans le contenu :
|
Écris en <strong>Markdown</strong> — les fichiers uploadés sont référençables dans le contenu :
|
||||||
@@ -64,55 +86,97 @@ $dateValue = isset($published_at)
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<?php if ($action === 'create'): ?>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="files" class="form-label">Ajouter des fichiers</label>
|
<label for="files" class="form-label">Ajouter des fichiers</label>
|
||||||
<input type="file" class="form-control" id="files" name="files[]" multiple>
|
<input type="file" class="form-control" id="files" name="files[]" multiple>
|
||||||
<div class="form-text">Images, vidéos, PDF… — intègre-les dans le contenu ou laisse-les en pièces jointes.</div>
|
<div class="form-text">Images → nommées <code>sha256-taille.ext</code>. Vidéos, PDF → nom sanitisé.</div>
|
||||||
</div>
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
<?php if ($action === 'edit' && !empty($existingFiles)): ?>
|
<?php if (!empty($existingFiles)): ?>
|
||||||
|
<?php $coverFile = $article['cover'] ?? ''; ?>
|
||||||
|
<?php $filesMeta = $article['files_meta'] ?? []; ?>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<p class="form-label">Fichiers existants</p>
|
<p class="form-label fw-semibold">Fichiers existants</p>
|
||||||
<div class="list-group">
|
<div class="list-group">
|
||||||
<?php foreach ($existingFiles as $i => $f): ?>
|
<?php foreach ($existingFiles as $i => $f): ?>
|
||||||
<?php $fileUrl = '/file?uuid=' . rawurlencode($uuid) . '&name=' . rawurlencode($f['name']); ?>
|
<?php
|
||||||
<div class="list-group-item d-flex align-items-center gap-3 py-2">
|
$fileUrl = '/file?uuid=' . rawurlencode($uuid) . '&name=' . rawurlencode($f['name']);
|
||||||
<?php if ($f['is_image']): ?>
|
$fmeta = $filesMeta[$f['name']] ?? [];
|
||||||
<a href="<?= htmlspecialchars($fileUrl) ?>" target="_blank" rel="noopener">
|
$isCoverFile = ($f['name'] === $coverFile);
|
||||||
<img src="<?= htmlspecialchars($fileUrl) ?>" alt=""
|
?>
|
||||||
style="width:48px;height:48px;object-fit:cover;border-radius:4px;flex-shrink:0">
|
<div class="list-group-item py-2">
|
||||||
</a>
|
<div class="d-flex align-items-center gap-3">
|
||||||
<?php else: ?>
|
<!-- Miniature -->
|
||||||
<a href="<?= htmlspecialchars($fileUrl) ?>" target="_blank" rel="noopener"
|
<?php if ($f['is_image']): ?>
|
||||||
style="width:48px;text-align:center;font-size:1.5rem;flex-shrink:0;text-decoration:none">
|
<a href="<?= htmlspecialchars($fileUrl) ?>" target="_blank" rel="noopener" class="flex-shrink-0">
|
||||||
<?php
|
<img src="<?= htmlspecialchars($fileUrl) ?>" alt=""
|
||||||
$icon = match(true) {
|
style="width:56px;height:56px;object-fit:cover;border-radius:4px;<?= $isCoverFile ? 'outline:2px solid #0d6efd' : '' ?>">
|
||||||
str_starts_with($f['mime'], 'video/') => '🎬',
|
</a>
|
||||||
str_starts_with($f['mime'], 'audio/') => '🎵',
|
<?php else: ?>
|
||||||
$f['mime'] === 'application/pdf' => '📑',
|
<span style="width:56px;text-align:center;font-size:1.6rem;flex-shrink:0">
|
||||||
default => '📄',
|
<?= match(true) {
|
||||||
};
|
str_starts_with($f['mime'], 'video/') => '🎬',
|
||||||
echo $icon;
|
str_starts_with($f['mime'], 'audio/') => '🎵',
|
||||||
?>
|
$f['mime'] === 'application/pdf' => '📑',
|
||||||
</a>
|
default => '📄',
|
||||||
<?php endif; ?>
|
} ?>
|
||||||
<div class="flex-grow-1 overflow-hidden">
|
</span>
|
||||||
<code class="d-block text-truncate"><?= htmlspecialchars($f['name']) ?></code>
|
<?php endif; ?>
|
||||||
<small class="text-muted">
|
|
||||||
<?= htmlspecialchars(number_format($f['size'] / 1024, 1)) ?> Ko
|
<!-- Infos + méta -->
|
||||||
— <?= htmlspecialchars($f['mime']) ?>
|
<div class="flex-grow-1 overflow-hidden">
|
||||||
</small>
|
<div class="d-flex align-items-center gap-2 mb-1">
|
||||||
</div>
|
<code class="text-truncate small"><?= htmlspecialchars($f['name']) ?></code>
|
||||||
<div class="d-flex gap-2 flex-shrink-0">
|
<small class="text-muted text-nowrap"><?= number_format($f['size'] / 1024, 1) ?> Ko</small>
|
||||||
<button type="button" class="btn btn-sm btn-outline-secondary"
|
<?php if ($isCoverFile): ?>
|
||||||
onclick="copyMdRef(<?= htmlspecialchars(json_encode($f['name'])) ?>, <?= $f['is_image'] ? 'true' : 'false' ?>, this)">
|
<span class="badge bg-primary">cover</span>
|
||||||
Référence MD
|
<?php endif; ?>
|
||||||
</button>
|
</div>
|
||||||
<button type="submit" form="del-file-<?= $i ?>"
|
<?php if ($f['is_image']): ?>
|
||||||
class="btn btn-sm btn-outline-danger"
|
<div class="d-flex gap-2 flex-wrap">
|
||||||
onclick="return confirm('Supprimer « <?= htmlspecialchars(addslashes($f['name'])) ?> » définitivement ?')">
|
<input type="hidden" name="fmeta_name[]" value="<?= htmlspecialchars($f['name']) ?>">
|
||||||
Supprimer
|
<input type="text" name="fmeta_author[]"
|
||||||
</button>
|
class="form-control form-control-sm"
|
||||||
|
style="max-width:220px"
|
||||||
|
placeholder="Auteur / crédit"
|
||||||
|
value="<?= htmlspecialchars($fmeta['author'] ?? '') ?>">
|
||||||
|
<input type="url" name="fmeta_source[]"
|
||||||
|
class="form-control form-control-sm font-monospace"
|
||||||
|
style="max-width:280px"
|
||||||
|
placeholder="URL source"
|
||||||
|
value="<?= htmlspecialchars($fmeta['source_url'] ?? '') ?>">
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Actions -->
|
||||||
|
<div class="d-flex flex-column gap-1 flex-shrink-0 align-items-end">
|
||||||
|
<?php if ($f['is_image'] && !$isCoverFile): ?>
|
||||||
|
<div class="form-check mb-0">
|
||||||
|
<input class="form-check-input" type="radio"
|
||||||
|
name="cover_file" id="cover_<?= $i ?>"
|
||||||
|
value="<?= htmlspecialchars($f['name']) ?>">
|
||||||
|
<label class="form-check-label small" for="cover_<?= $i ?>">Cover</label>
|
||||||
|
</div>
|
||||||
|
<?php elseif ($isCoverFile): ?>
|
||||||
|
<input type="hidden" name="cover_file" value="<?= htmlspecialchars($f['name']) ?>">
|
||||||
|
<small class="text-primary">✓ Cover</small>
|
||||||
|
<?php endif; ?>
|
||||||
|
<div class="d-flex gap-1">
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-secondary"
|
||||||
|
data-copy-md-name="<?= htmlspecialchars($fmeta['title'] ?? $f['name']) ?>"
|
||||||
|
data-copy-md-is-image="<?= $f['is_image'] ? '1' : '0' ?>">
|
||||||
|
MD
|
||||||
|
</button>
|
||||||
|
<button type="submit" form="del-file-<?= $i ?>"
|
||||||
|
class="btn btn-sm btn-outline-danger"
|
||||||
|
data-confirm="Supprimer « <?= htmlspecialchars($f['name']) ?> » ?">
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
@@ -179,8 +243,13 @@ $dateValue = isset($published_at)
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button type="submit" class="btn btn-success">Enregistrer</button>
|
<div class="d-flex align-items-center gap-3 flex-wrap">
|
||||||
<a href="/" class="btn btn-secondary">Annuler</a>
|
<button type="submit" class="btn btn-success">Enregistrer</button>
|
||||||
|
<a href="/" class="btn btn-secondary">Annuler</a>
|
||||||
|
<?php if ($action === 'edit'): ?>
|
||||||
|
<span id="autosave-indicator" class="text-muted small"></span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<?php if ($action === 'edit' && !empty($existingFiles)): ?>
|
<?php if ($action === 'edit' && !empty($existingFiles)): ?>
|
||||||
@@ -192,41 +261,125 @@ $dateValue = isset($published_at)
|
|||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<script>
|
<?php if ($action === 'edit' && !empty($article['revisions'])): ?>
|
||||||
function slugify(s) {
|
<hr class="my-4">
|
||||||
const map = {'à':'a','â':'a','ä':'a','é':'e','è':'e','ê':'e','ë':'e','î':'i','ï':'i','ô':'o','ö':'o','ù':'u','û':'u','ü':'u','ç':'c','æ':'ae','œ':'oe'};
|
<div>
|
||||||
return s.toLowerCase().replace(/[àâäéèêëîïôöùûüçæœ]/g, c => map[c] || c)
|
<button class="btn btn-sm btn-link text-secondary text-decoration-none p-0 fw-semibold"
|
||||||
.replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
|
type="button" data-bs-toggle="collapse" data-bs-target="#historyPanel">
|
||||||
}
|
▸ Historique des révisions (<?= count($article['revisions']) ?>)
|
||||||
function autoSlug(title) {
|
</button>
|
||||||
const slugField = document.getElementById('slug');
|
<div class="collapse mt-3" id="historyPanel">
|
||||||
const preview = document.getElementById('slug-preview');
|
<table class="table table-sm table-hover align-middle">
|
||||||
// N'écrase le slug que s'il est vide ou s'il correspond à la génération automatique
|
<thead>
|
||||||
if (slugField._auto !== false) {
|
<tr><th>#</th><th>Date</th><th>Titre à l'époque</th><th>Commentaire</th><th></th></tr>
|
||||||
const generated = slugify(title);
|
</thead>
|
||||||
slugField.value = generated;
|
<tbody>
|
||||||
preview.textContent = generated;
|
<?php foreach (array_reverse($article['revisions']) as $rev): ?>
|
||||||
}
|
<tr>
|
||||||
}
|
<td class="text-muted small"><?= (int)($rev['n'] ?? 0) ?></td>
|
||||||
document.getElementById('slug').addEventListener('input', function() {
|
<td class="small text-nowrap">
|
||||||
this._auto = (this.value === '');
|
<?= htmlspecialchars(date('d/m/Y H:i', strtotime((string)($rev['date'] ?? '')))) ?>
|
||||||
document.getElementById('slug-preview').textContent = this.value;
|
</td>
|
||||||
});
|
<td class="small text-truncate" style="max-width:200px">
|
||||||
// En mode édition le champ est pré-rempli : désactive l'auto-génération
|
<?= htmlspecialchars($rev['title'] ?? '') ?>
|
||||||
(function() {
|
</td>
|
||||||
const s = document.getElementById('slug');
|
<td class="small text-muted">
|
||||||
if (s.value !== '') s._auto = false;
|
<?= htmlspecialchars($rev['comment'] ?? '') ?: '<span class="text-muted">–</span>' ?>
|
||||||
})();
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href="/?action=diff&uuid=<?= rawurlencode($uuid) ?>&rev=<?= (int)($rev['n'] ?? 0) ?>"
|
||||||
|
class="btn btn-outline-secondary btn-sm" target="_blank">
|
||||||
|
Diff
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ($action === 'edit'): ?>
|
||||||
|
</div><!-- /col-lg-8 -->
|
||||||
|
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<?php $sidebarImages = array_filter($existingFiles ?? [], fn ($f) => $f['is_image']); ?>
|
||||||
|
|
||||||
|
<?php if ($sidebarImages): ?>
|
||||||
|
<div class="mb-3">
|
||||||
|
<p class="fw-semibold small mb-2">
|
||||||
|
Images disponibles
|
||||||
|
<span class="text-muted fw-normal">(clic → insère dans le contenu)</span>
|
||||||
|
</p>
|
||||||
|
<div class="d-flex flex-wrap gap-2">
|
||||||
|
<?php foreach ($sidebarImages as $img): ?>
|
||||||
|
<?php $imgUrl = '/file?uuid=' . rawurlencode($uuid) . '&name=' . rawurlencode($img['name']); ?>
|
||||||
|
<img src="<?= htmlspecialchars($imgUrl) ?>"
|
||||||
|
alt="<?= htmlspecialchars($img['name']) ?>"
|
||||||
|
title="<?= htmlspecialchars($img['name']) ?>"
|
||||||
|
data-insert-ref="<?= htmlspecialchars($img['name']) ?>"
|
||||||
|
style="width:72px;height:72px;object-fit:cover;border-radius:6px;cursor:pointer;border:2px solid transparent;transition:border-color .15s">
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php $externalLinks = $article['external_links'] ?? []; ?>
|
||||||
|
<?php if ($externalLinks): ?>
|
||||||
|
<div class="mb-3">
|
||||||
|
<p class="fw-semibold small mb-2">Liens externes</p>
|
||||||
|
<ul class="list-group list-group-flush">
|
||||||
|
<?php foreach ($externalLinks as $extLink): ?>
|
||||||
|
<?php
|
||||||
|
$elUrl = $extLink['url'];
|
||||||
|
$elName = $extLink['name'];
|
||||||
|
$elIsImg = (bool)preg_match('/\.(jpe?g|png|gif|webp|svg|avif)(\?.*)?$/i', $elUrl);
|
||||||
|
?>
|
||||||
|
<li class="list-group-item px-0 py-1 d-flex align-items-center gap-2 border-0 border-bottom">
|
||||||
|
<span class="flex-shrink-0" style="font-size:1rem">
|
||||||
|
<?= $elIsImg ? '🖼' : '📄' ?>
|
||||||
|
</span>
|
||||||
|
<span class="flex-grow-1 text-truncate small"
|
||||||
|
title="<?= htmlspecialchars($elUrl) ?>"
|
||||||
|
data-insert-ref="<?= htmlspecialchars($elUrl) ?>"
|
||||||
|
style="cursor:pointer;color:#0d6efd;text-decoration:underline dotted">
|
||||||
|
<?= htmlspecialchars($elName) ?>
|
||||||
|
</span>
|
||||||
|
<form method="POST" action="/?action=delete_external_link&uuid=<?= rawurlencode($uuid) ?>"
|
||||||
|
class="d-inline flex-shrink-0">
|
||||||
|
<input type="hidden" name="url" value="<?= htmlspecialchars($elUrl) ?>">
|
||||||
|
<button type="submit" class="btn btn-link btn-sm text-danger p-0 lh-1"
|
||||||
|
data-confirm="Supprimer ce lien externe ?"
|
||||||
|
title="Supprimer">✕</button>
|
||||||
|
</form>
|
||||||
|
</li>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div class="d-flex flex-column gap-2">
|
||||||
|
<a href="/?action=add_files&uuid=<?= rawurlencode($uuid) ?>" class="btn btn-outline-secondary btn-sm">
|
||||||
|
+ Ajouter des fichiers
|
||||||
|
</a>
|
||||||
|
<a href="/?action=import_image&uuid=<?= rawurlencode($uuid) ?>" class="btn btn-outline-secondary btn-sm">
|
||||||
|
+ Importer depuis une URL
|
||||||
|
</a>
|
||||||
|
<?php
|
||||||
|
$hasSources = !empty($article['external_links']) || !empty($existingFiles);
|
||||||
|
if ($hasSources):
|
||||||
|
?>
|
||||||
|
<a href="/?action=sources&uuid=<?= rawurlencode($uuid) ?>" class="btn btn-outline-secondary btn-sm">
|
||||||
|
Sources & métadonnées
|
||||||
|
</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div><!-- /col-lg-4 -->
|
||||||
|
|
||||||
|
</div><!-- /row -->
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
function copyMdRef(name, isImage, btn) {
|
|
||||||
const ref = isImage ? `` : `[${name}](${name})`;
|
|
||||||
navigator.clipboard.writeText(ref).then(() => {
|
|
||||||
const orig = btn.textContent;
|
|
||||||
btn.textContent = 'Copié !';
|
|
||||||
setTimeout(() => { btn.textContent = orig; }, 1500);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<?php
|
<?php
|
||||||
$content = ob_get_clean();
|
$content = ob_get_clean();
|
||||||
|
|||||||
@@ -2,15 +2,6 @@
|
|||||||
require_once BASE_PATH . '/src/Parsedown.php';
|
require_once BASE_PATH . '/src/Parsedown.php';
|
||||||
$Parsedown = new Parsedown();
|
$Parsedown = new Parsedown();
|
||||||
|
|
||||||
$coverGradients = [
|
|
||||||
'linear-gradient(135deg, #e0e7ff 0%, #c7d2fe 100%)',
|
|
||||||
'linear-gradient(135deg, #d1fae5 0%, #a7f3d0 100%)',
|
|
||||||
'linear-gradient(135deg, #fce7f3 0%, #fbcfe8 100%)',
|
|
||||||
'linear-gradient(135deg, #fef3c7 0%, #fde68a 100%)',
|
|
||||||
'linear-gradient(135deg, #dbeafe 0%, #bfdbfe 100%)',
|
|
||||||
'linear-gradient(135deg, #ede9fe 0%, #ddd6fe 100%)',
|
|
||||||
];
|
|
||||||
|
|
||||||
ob_start();
|
ob_start();
|
||||||
?>
|
?>
|
||||||
|
|
||||||
@@ -19,10 +10,13 @@ ob_start();
|
|||||||
<?php
|
<?php
|
||||||
$html = $Parsedown->text($post['content']);
|
$html = $Parsedown->text($post['content']);
|
||||||
$preview = mb_strimwidth(strip_tags($html), 0, 120, '…');
|
$preview = mb_strimwidth(strip_tags($html), 0, 120, '…');
|
||||||
$gradient = $coverGradients[$i % count($coverGradients)];
|
$category = trim((string)($post['category'] ?? ''));
|
||||||
|
$gradient = coverGradient($category !== '' ? $category : $post['uuid'], $allCats ?? []);
|
||||||
$postUrl = '/post/' . rawurlencode($post['slug']);
|
$postUrl = '/post/' . rawurlencode($post['slug']);
|
||||||
$isDraft = !$post['published'];
|
$isDraft = !$post['published'];
|
||||||
$isAvantPremiere = $post['published'] && strtotime((string)($post['published_at'] ?? '')) > time();
|
$isAvantPremiere = $post['published'] && strtotime((string)($post['published_at'] ?? '')) > time();
|
||||||
|
$postCat = trim($post['category'] ?? '');
|
||||||
|
$isPrivate = $postCat !== '' && in_array($postCat, $privateCats ?? [], true);
|
||||||
$isLocked = $isAvantPremiere;
|
$isLocked = $isAvantPremiere;
|
||||||
?>
|
?>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
@@ -31,6 +25,8 @@ ob_start();
|
|||||||
<div class="draft-ribbon">Brouillon</div>
|
<div class="draft-ribbon">Brouillon</div>
|
||||||
<?php elseif ($isAvantPremiere): ?>
|
<?php elseif ($isAvantPremiere): ?>
|
||||||
<div class="premiere-ribbon">Avant-première</div>
|
<div class="premiere-ribbon">Avant-première</div>
|
||||||
|
<?php elseif ($isPrivate): ?>
|
||||||
|
<div class="private-ribbon">Privé</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<?php
|
<?php
|
||||||
$coverFile = $post['cover'] ?? '';
|
$coverFile = $post['cover'] ?? '';
|
||||||
@@ -38,7 +34,11 @@ ob_start();
|
|||||||
? 'background-image: url(\'/file?uuid=' . rawurlencode($post['uuid']) . '&name=' . rawurlencode($coverFile) . '\')'
|
? 'background-image: url(\'/file?uuid=' . rawurlencode($post['uuid']) . '&name=' . rawurlencode($coverFile) . '\')'
|
||||||
: 'background: ' . $gradient;
|
: 'background: ' . $gradient;
|
||||||
?>
|
?>
|
||||||
<div class="card-cover" style="<?= $coverStyle ?>"></div>
|
<div class="card-cover" style="<?= $coverStyle ?>">
|
||||||
|
<?php if ($category !== ''): ?>
|
||||||
|
<span class="cover-category"><?= htmlspecialchars($category) ?></span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
<div class="card-body d-flex flex-column">
|
<div class="card-body d-flex flex-column">
|
||||||
<h2 class="card-title">
|
<h2 class="card-title">
|
||||||
<?php if ($isLocked): ?>
|
<?php if ($isLocked): ?>
|
||||||
@@ -72,6 +72,21 @@ ob_start();
|
|||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<?php if ($prevCursor !== null || $nextCursor !== null): ?>
|
||||||
|
<nav class="pagination-nav mt-5" aria-label="Navigation">
|
||||||
|
<?php
|
||||||
|
$catParam = $filterCat !== '' ? 'cat=' . rawurlencode($filterCat) . '&' : '';
|
||||||
|
?>
|
||||||
|
<?php if ($prevCursor !== null): ?>
|
||||||
|
<?php $prevHref = $prevCursor === '' ? '/?' . rtrim($catParam, '&') : '/?' . $catParam . 'cursor=' . rawurlencode($prevCursor); ?>
|
||||||
|
<a class="pagination-btn" href="<?= htmlspecialchars($prevHref) ?>">← Plus récents</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if ($nextCursor !== null): ?>
|
||||||
|
<a class="pagination-btn ms-auto" href="/?<?= $catParam ?>cursor=<?= rawurlencode($nextCursor) ?>">Plus anciens →</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
</nav>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
<?php
|
<?php
|
||||||
$content = ob_get_clean();
|
$content = ob_get_clean();
|
||||||
$title = 'varlog';
|
$title = 'varlog';
|
||||||
|
|||||||
@@ -3,89 +3,254 @@ require_once __DIR__ . '/../src/Parsedown.php';
|
|||||||
$Parsedown = new Parsedown();
|
$Parsedown = new Parsedown();
|
||||||
|
|
||||||
ob_start();
|
ob_start();
|
||||||
?>
|
|
||||||
|
|
||||||
<a href="/" class="btn btn-secondary mb-3">← Retour</a>
|
|
||||||
|
|
||||||
<?php
|
|
||||||
$coverFile = $article['cover'] ?? '';
|
$coverFile = $article['cover'] ?? '';
|
||||||
$ogImage = $coverFile !== ''
|
$ogImage = $coverFile !== ''
|
||||||
? url('file?uuid=' . rawurlencode($article['uuid']) . '&name=' . rawurlencode($coverFile))
|
? url('file?uuid=' . rawurlencode($article['uuid']) . '&name=' . rawurlencode($coverFile))
|
||||||
: null;
|
: null;
|
||||||
?>
|
|
||||||
<div class="card mb-4">
|
|
||||||
<?php if (!$article['published']): ?>
|
|
||||||
<div class="draft-ribbon">Brouillon</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
<?php if ($coverFile !== ''): ?>
|
|
||||||
<div class="article-cover">
|
|
||||||
<img src="/file?uuid=<?= rawurlencode($article['uuid']) ?>&name=<?= rawurlencode($coverFile) ?>"
|
|
||||||
alt="<?= htmlspecialchars($article['title']) ?>">
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
<div class="card-body">
|
|
||||||
<h2 class="card-title"><?= htmlspecialchars($article['title']) ?></h2>
|
|
||||||
|
|
||||||
<div class="card-text post-content">
|
$category = trim((string)($article['category'] ?? ''));
|
||||||
<?= $Parsedown->text($rawContent) ?>
|
$gradient = coverGradient($category !== '' ? $category : $article['uuid'], $allCats ?? []);
|
||||||
</div>
|
|
||||||
|
|
||||||
<p class="text-muted small mt-2">
|
// Pièces jointes (hors fichiers intégrés, thumbs et cover)
|
||||||
Publié le <?= htmlspecialchars(date('d/m/Y H:i', strtotime((string)($article['published_at'] ?? $article['created_at'] ?? '')))) ?>
|
$attachments = [];
|
||||||
</p>
|
if ($files) {
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<?php if ($files): ?>
|
|
||||||
<?php
|
|
||||||
// Sépare les fichiers intégrés (référencés dans le contenu) des pièces jointes
|
|
||||||
$referenced = [];
|
$referenced = [];
|
||||||
preg_match_all('/\(\/file\?uuid=[^&]+&name=([^)]+)\)/', $rawContent, $m);
|
preg_match_all('/\(\/file\?uuid=[^&]+&name=([^)]+)\)/', $rawContent, $m);
|
||||||
foreach ($m[1] as $encodedName) {
|
foreach ($m[1] as $encodedName) {
|
||||||
$referenced[rawurldecode($encodedName)] = true;
|
$referenced[rawurldecode($encodedName)] = true;
|
||||||
}
|
}
|
||||||
$attachments = array_filter($files, static fn ($f) => !isset($referenced[$f['name']]));
|
$attachments = array_values(array_filter(
|
||||||
?>
|
$files,
|
||||||
<?php if ($attachments): ?>
|
static fn ($f) =>
|
||||||
<section class="mb-4">
|
!isset($referenced[$f['name']])
|
||||||
<h5>Pièces jointes</h5>
|
&& !str_starts_with($f['name'], '_thumb_')
|
||||||
<div class="row g-3">
|
&& $f['name'] !== $coverFile
|
||||||
<?php foreach ($attachments as $file): ?>
|
));
|
||||||
<div class="col-sm-6 col-md-4">
|
}
|
||||||
<div class="card">
|
|
||||||
<?php
|
$externalLinks = $article['external_links'] ?? [];
|
||||||
$fileUrl = '/file?uuid=' . rawurlencode($article['uuid']) . '&name=' . rawurlencode($file['name']);
|
$hasLeftSidebar = !empty($categorySidebar ?? []);
|
||||||
?>
|
?>
|
||||||
<?php if ($file['is_image']): ?>
|
<div class="row g-4 align-items-start flex-nowrap-lg">
|
||||||
<img src="<?= htmlspecialchars($fileUrl) ?>" class="card-img-top" alt="<?= htmlspecialchars($file['name']) ?>" style="max-height:200px;object-fit:cover">
|
|
||||||
<?php elseif ($file['is_video']): ?>
|
<?php if ($hasLeftSidebar): ?>
|
||||||
<video controls class="w-100" style="max-height:200px"><source src="<?= htmlspecialchars($fileUrl) ?>"></video>
|
<div class="post-sidebar-col order-2 order-lg-1">
|
||||||
<?php elseif ($file['is_audio']): ?>
|
<aside class="left-sidebar">
|
||||||
<audio controls class="w-100"><source src="<?= htmlspecialchars($fileUrl) ?>"></audio>
|
<?php foreach ($categorySidebar as $catName => $catArticles): ?>
|
||||||
<?php endif; ?>
|
<div class="left-sidebar-section">
|
||||||
<div class="card-body p-2">
|
<a href="/?cat=<?= rawurlencode($catName) ?>" class="left-sidebar-cat">
|
||||||
<a href="<?= htmlspecialchars($fileUrl) ?>" class="card-title small d-block text-truncate" target="_blank">
|
<?= htmlspecialchars($catName) ?>
|
||||||
<?= htmlspecialchars($file['name']) ?>
|
</a>
|
||||||
</a>
|
<ul class="left-sidebar-list">
|
||||||
<small class="text-muted"><?= htmlspecialchars(number_format($file['size'] / 1024, 1)) ?> Ko</small>
|
<?php foreach ($catArticles as $ca): ?>
|
||||||
</div>
|
<li>
|
||||||
</div>
|
<a href="/post/<?= rawurlencode($ca['slug'] ?? '') ?>">
|
||||||
</div>
|
<?= htmlspecialchars($ca['title']) ?>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</div>
|
</ul>
|
||||||
</section>
|
</div>
|
||||||
<?php endif; ?>
|
<?php endforeach; ?>
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<?php if (function_exists('isAdmin') && isAdmin()): ?>
|
<!-- Colonne principale -->
|
||||||
<div class="d-flex gap-2 mt-3">
|
<div class="col order-1 order-lg-2">
|
||||||
<a href="/?action=edit&uuid=<?= htmlspecialchars($article['uuid']) ?>" class="btn btn-primary">Modifier</a>
|
|
||||||
<a href="/?action=delete&uuid=<?= htmlspecialchars($article['uuid']) ?>"
|
<div class="card mb-4">
|
||||||
class="btn btn-danger"
|
<?php if (!$article['published']): ?>
|
||||||
onclick="return confirm('Supprimer cet article définitivement ?')">Supprimer</a>
|
<div class="draft-ribbon">Brouillon</div>
|
||||||
|
<?php elseif ($isPrivateCat ?? false): ?>
|
||||||
|
<div class="private-ribbon">Privé</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php
|
||||||
|
$authorEmail = $article['author'] ?? '';
|
||||||
|
$authorName = ($authorEmail !== '' && function_exists('authorDisplayName')) ? authorDisplayName($authorEmail) : '';
|
||||||
|
$pubDate = htmlspecialchars(date('d/m/Y', strtotime((string)($article['published_at'] ?? $article['created_at'] ?? ''))));
|
||||||
|
$hasCover = $coverFile !== '';
|
||||||
|
$heroExtraClass = $hasCover ? '' : ' article-cover--gradient';
|
||||||
|
$heroStyle = $hasCover ? '' : ' style="background:' . htmlspecialchars($gradient) . '"';
|
||||||
|
$hasSources = (!empty($externalLinks) || !empty($files))
|
||||||
|
&& function_exists('canDoOnArticle') && canDoOnArticle('view_sources', $article);
|
||||||
|
?>
|
||||||
|
<div class="article-cover article-cover--hero<?= $heroExtraClass ?>"<?= $heroStyle ?>>
|
||||||
|
<?php if ($hasCover): ?>
|
||||||
|
<img src="/file?uuid=<?= rawurlencode($article['uuid']) ?>&name=<?= rawurlencode($coverFile) ?>"
|
||||||
|
alt="<?= htmlspecialchars($article['title']) ?>">
|
||||||
|
<?php endif; ?>
|
||||||
|
<div class="article-hero-text">
|
||||||
|
|
||||||
|
<!-- Haut : retour + actions admin -->
|
||||||
|
<div class="article-hero-top">
|
||||||
|
<a href="/" class="hero-btn">← Retour</a>
|
||||||
|
<?php if (function_exists('isAdmin') && isAdmin()): ?>
|
||||||
|
<a href="/?action=edit&uuid=<?= rawurlencode($article['uuid']) ?>" class="hero-btn ms-auto">✎ Modifier</a>
|
||||||
|
<a href="/?action=delete&uuid=<?= rawurlencode($article['uuid']) ?>"
|
||||||
|
class="hero-btn hero-btn--danger"
|
||||||
|
data-confirm="Supprimer cet article définitivement ?">🗑 Supprimer</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Bas : titre + actions secondaires -->
|
||||||
|
<div class="article-hero-bottom">
|
||||||
|
<div class="article-hero-left">
|
||||||
|
<?php if ($category !== ''): ?>
|
||||||
|
<span class="cover-category"><?= htmlspecialchars($category) ?></span>
|
||||||
|
<?php endif; ?>
|
||||||
|
<h1 class="article-title"><?= htmlspecialchars($article['title']) ?></h1>
|
||||||
|
<p class="article-hero-meta">
|
||||||
|
<?php if ($authorName !== ''): ?>
|
||||||
|
<span><?= htmlspecialchars($authorName) ?></span>
|
||||||
|
<span class="mx-1 opacity-50">·</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?= $pubDate ?>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="article-hero-right">
|
||||||
|
<?php if ($hasSources): ?>
|
||||||
|
<a href="/?action=sources&uuid=<?= rawurlencode($article['uuid']) ?>" class="hero-btn">ℹ Sources</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if (function_exists('isLoggedIn') && isLoggedIn()): ?>
|
||||||
|
<form method="post" action="/?action=rate" class="d-flex align-items-center gap-2">
|
||||||
|
<input type="hidden" name="uuid" value="<?= htmlspecialchars($article['uuid']) ?>">
|
||||||
|
<?php if ($ratingStats['count'] > 0): ?>
|
||||||
|
<span class="hero-rating-score">
|
||||||
|
<?= number_format((float)($ratingStats['avg'] ?? 0), 1) ?> <span style="opacity:.6">/ 5</span>
|
||||||
|
</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
<div class="star-rating star-rating--hero">
|
||||||
|
<?php for ($s = 5; $s >= 1; $s--): ?>
|
||||||
|
<input type="radio" id="star<?= $s ?>-<?= $article['uuid'] ?>"
|
||||||
|
name="rating" value="<?= $s ?>"
|
||||||
|
<?= (int)($userRating ?? 0) === $s ? 'checked' : '' ?>
|
||||||
|
onchange="this.form.submit()">
|
||||||
|
<label for="star<?= $s ?>-<?= $article['uuid'] ?>" title="<?= $s ?>★">★</label>
|
||||||
|
<?php endfor; ?>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<?php elseif ($ratingStats['count'] > 0): ?>
|
||||||
|
<span class="hero-rating-score">
|
||||||
|
★ <?= number_format((float)($ratingStats['avg'] ?? 0), 1) ?>
|
||||||
|
<span style="opacity:.6">(<?= $ratingStats['count'] ?>)</span>
|
||||||
|
</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="card-text post-content">
|
||||||
|
<?= $Parsedown->text($rawContent) ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if (!isLoggedIn() && $ratingStats['count'] > 0): ?>
|
||||||
|
<p class="text-muted small mt-3">
|
||||||
|
Note : <?= number_format((float)($ratingStats['avg'] ?? 0), 1) ?>/5
|
||||||
|
— <a href="/login">Connectez-vous</a> pour noter.
|
||||||
|
</p>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
|
</div><!-- /col principale -->
|
||||||
|
|
||||||
|
<div class="post-sidebar-col order-3">
|
||||||
|
<aside class="related-sidebar">
|
||||||
|
|
||||||
|
<?php if (!empty($attachments)): ?>
|
||||||
|
<h6 class="related-sidebar-title">Pièces jointes</h6>
|
||||||
|
<div class="d-flex flex-column gap-2 mb-4">
|
||||||
|
<?php foreach ($attachments as $file):
|
||||||
|
$fileUrl = '/file?uuid=' . rawurlencode($article['uuid']) . '&name=' . rawurlencode($file['name']);
|
||||||
|
?>
|
||||||
|
<a href="<?= htmlspecialchars($fileUrl) ?>" target="_blank" rel="noopener" class="source-card">
|
||||||
|
<?php if ($file['is_image']): ?>
|
||||||
|
<div class="source-card-thumb" style="background-image:url(<?= htmlspecialchars($fileUrl) ?>);background-size:cover;background-position:center"></div>
|
||||||
|
<?php elseif ($file['is_video']): ?>
|
||||||
|
<div class="source-card-thumb source-card-thumb--link">▶</div>
|
||||||
|
<?php elseif ($file['is_audio']): ?>
|
||||||
|
<div class="source-card-thumb source-card-thumb--link">♪</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="source-card-thumb source-card-thumb--pdf">📎</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<div class="source-card-body">
|
||||||
|
<div class="source-card-title"><?= htmlspecialchars($file['name']) ?></div>
|
||||||
|
<div class="source-card-meta"><?= htmlspecialchars(number_format($file['size'] / 1024, 1)) ?> Ko</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if (!empty($externalLinks)): ?>
|
||||||
|
<h6 class="related-sidebar-title">Liens & sources</h6>
|
||||||
|
<div class="d-flex flex-column gap-2 mb-4">
|
||||||
|
<?php foreach ($externalLinks as $lnk):
|
||||||
|
$lMeta = $lnk['meta'] ?? [];
|
||||||
|
$lTitle = $lnk['name'] ?? '';
|
||||||
|
$lUrl = $lnk['url'] ?? '';
|
||||||
|
$lHost = parse_url($lUrl, PHP_URL_HOST) ?? $lUrl;
|
||||||
|
$lDate = $lMeta['date'] ?? '';
|
||||||
|
$lSite = $lMeta['site_name'] ?? $lHost;
|
||||||
|
$lImage = $lMeta['og_image'] ?? '';
|
||||||
|
$lMime = $lMeta['mime'] ?? 'text/html';
|
||||||
|
$lPages = $lMeta['pages'] ?? null;
|
||||||
|
$lFormat = $lMeta['page_size'] ?? '';
|
||||||
|
$isPdf = ($lMime === 'application/pdf');
|
||||||
|
?>
|
||||||
|
<a href="<?= htmlspecialchars($lUrl) ?>" target="_blank" rel="noopener" class="source-card">
|
||||||
|
<?php if ($lImage && str_starts_with($lImage, '/')): ?>
|
||||||
|
<div class="source-card-thumb" style="background-image:url(<?= htmlspecialchars($lImage) ?>);background-size:cover;background-position:center"></div>
|
||||||
|
<?php elseif ($isPdf): ?>
|
||||||
|
<div class="source-card-thumb source-card-thumb--pdf">📑</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="source-card-thumb source-card-thumb--link">↗</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<div class="source-card-body">
|
||||||
|
<div class="source-card-title"><?= htmlspecialchars($lTitle) ?></div>
|
||||||
|
<div class="source-card-meta">
|
||||||
|
<?= htmlspecialchars($lSite) ?>
|
||||||
|
<?php if ($lDate): ?> · <?= htmlspecialchars(substr($lDate, 0, 10)) ?><?php endif; ?>
|
||||||
|
<?php if ($isPdf && $lPages): ?> · PDF <?= $lPages ?>p.<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<h6 class="related-sidebar-title">Dans la même catégorie</h6>
|
||||||
|
<?php if (!empty($relatedArticles ?? [])): ?>
|
||||||
|
<?php foreach ($relatedArticles as $rel):
|
||||||
|
$relCover = $rel['cover'] ?? '';
|
||||||
|
$relCat = trim($rel['category'] ?? '');
|
||||||
|
$relGradient = coverGradient($relCat !== '' ? $relCat : $rel['uuid'], $allCats ?? []);
|
||||||
|
$relDate = date('d/m/Y', strtotime((string)($rel['published_at'] ?? $rel['created_at'] ?? '')));
|
||||||
|
?>
|
||||||
|
<a href="/post/<?= rawurlencode($rel['slug'] ?? '') ?>" class="related-card">
|
||||||
|
<div class="related-card-thumb" style="<?= $relCover !== ''
|
||||||
|
? 'background-image:url(/file?uuid=' . rawurlencode($rel['uuid']) . '&name=' . rawurlencode($relCover) . ');background-size:cover;background-position:center'
|
||||||
|
: 'background:' . htmlspecialchars($relGradient) ?>">
|
||||||
|
</div>
|
||||||
|
<div class="related-card-body">
|
||||||
|
<div class="related-card-title"><?= htmlspecialchars($rel['title']) ?></div>
|
||||||
|
<div class="related-card-date"><?= $relDate ?></div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php else: ?>
|
||||||
|
<p class="text-muted small">Aucun autre article dans cette catégorie.</p>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div><!-- /row -->
|
||||||
|
|
||||||
<?php
|
<?php
|
||||||
$content = ob_get_clean();
|
$content = ob_get_clean();
|
||||||
$title = htmlspecialchars($article['title']);
|
$title = htmlspecialchars($article['title']);
|
||||||
@@ -95,4 +260,5 @@ $ogImage = $article['og_image'] ?? '';
|
|||||||
$ogType = 'article';
|
$ogType = 'article';
|
||||||
$ogUrl = url('post/' . rawurlencode($article['slug'] ?? ''));
|
$ogUrl = url('post/' . rawurlencode($article['slug'] ?? ''));
|
||||||
$articlePublishedAt = $article['published_at'] ?? '';
|
$articlePublishedAt = $article['published_at'] ?? '';
|
||||||
|
$mainClass = 'container-fluid';
|
||||||
include __DIR__ . '/layout.php';
|
include __DIR__ . '/layout.php';
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
<?php ob_start(); ?>
|
||||||
|
|
||||||
|
<div class="d-flex align-items-center gap-3 mb-4">
|
||||||
|
<h1 class="h4 mb-0">Mon profil</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 col-lg-4">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<?php if ($profileSuccess): ?>
|
||||||
|
<div class="alert alert-success py-2 small mb-3">Profil mis à jour.</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if ($profileError !== ''): ?>
|
||||||
|
<div class="alert alert-danger py-2 small mb-3"><?= htmlspecialchars($profileError) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<form method="post" action="/?action=profile">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label fw-semibold" for="display_name">Nom affiché</label>
|
||||||
|
<input type="text" id="display_name" name="display_name"
|
||||||
|
class="form-control"
|
||||||
|
value="<?= htmlspecialchars($profileCurrentName) ?>"
|
||||||
|
placeholder="Prénom Nom" required>
|
||||||
|
<div class="form-text">Affiché comme auteur sur vos articles.</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label fw-semibold text-muted">Email</label>
|
||||||
|
<input type="text" class="form-control" value="<?= htmlspecialchars(currentUserEmail() ?? '') ?>" disabled>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary w-100">Enregistrer</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
$content = ob_get_clean();
|
||||||
|
$title = 'Mon profil';
|
||||||
|
include __DIR__ . '/layout.php';
|
||||||
@@ -0,0 +1,242 @@
|
|||||||
|
<?php ob_start();
|
||||||
|
|
||||||
|
$metaLabels = [
|
||||||
|
'mime' => 'Type MIME',
|
||||||
|
'size' => 'Taille originale',
|
||||||
|
'pages' => 'Pages',
|
||||||
|
'page_size' => 'Format',
|
||||||
|
'pdf_version' => 'Version PDF',
|
||||||
|
'width' => 'Dimensions',
|
||||||
|
'camera' => 'Appareil photo',
|
||||||
|
'site_name' => 'Site',
|
||||||
|
'og_type' => 'Type OG',
|
||||||
|
'language' => 'Langue',
|
||||||
|
'date' => 'Date',
|
||||||
|
'description' => 'Description',
|
||||||
|
'subject' => 'Sujet',
|
||||||
|
'keywords' => 'Mots-clés',
|
||||||
|
'copyright' => 'Copyright',
|
||||||
|
'credit' => 'Crédit',
|
||||||
|
'creator' => 'Créé avec',
|
||||||
|
'producer' => 'Produit par',
|
||||||
|
'canonical' => 'URL canonique',
|
||||||
|
'og_image' => 'Image OG',
|
||||||
|
];
|
||||||
|
|
||||||
|
function renderMetaCell(string $key, mixed $val, array $row = []): string
|
||||||
|
{
|
||||||
|
return match($key) {
|
||||||
|
'size' => htmlspecialchars(number_format((float)$val / 1024, 1)) . ' Ko',
|
||||||
|
'width' => htmlspecialchars((string)$val) . ' × ' . htmlspecialchars((string)($row['height'] ?? '?')) . ' px',
|
||||||
|
'og_image' => str_starts_with((string)$val, '/')
|
||||||
|
? '<img src="' . htmlspecialchars((string)$val) . '" style="max-height:64px;max-width:160px;border-radius:4px" alt="">'
|
||||||
|
: '<a href="' . htmlspecialchars((string)$val) . '" target="_blank" rel="noopener" class="small text-break font-monospace">' . htmlspecialchars((string)$val) . '</a>',
|
||||||
|
'canonical' => '<a href="' . htmlspecialchars((string)$val) . '" target="_blank" rel="noopener" class="text-break font-monospace small">' . htmlspecialchars((string)$val) . '</a>',
|
||||||
|
default => htmlspecialchars((string)$val),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="d-flex align-items-center gap-3 mb-1">
|
||||||
|
<a href="/?action=edit&uuid=<?= rawurlencode($article['uuid']) ?>" class="btn btn-secondary btn-sm">← Modifier</a>
|
||||||
|
<h1 class="h4 mb-0">Sources & médias</h1>
|
||||||
|
</div>
|
||||||
|
<p class="text-muted small mb-4"><?= htmlspecialchars($article['title']) ?></p>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
// ── Liens & sources externes ──────────────────────────────────────────────────
|
||||||
|
$externalLinks = $article['external_links'] ?? [];
|
||||||
|
?>
|
||||||
|
<section class="mb-5">
|
||||||
|
<h2 class="h5 border-bottom pb-2 mb-3">
|
||||||
|
Liens & sources externes
|
||||||
|
<span class="badge bg-secondary fw-normal ms-1"><?= count($externalLinks) ?></span>
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<?php if (empty($externalLinks)): ?>
|
||||||
|
<p class="text-muted">Aucun lien externe enregistré.</p>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="d-flex flex-column gap-3">
|
||||||
|
<?php foreach ($externalLinks as $lnk):
|
||||||
|
$lMeta = $lnk['meta'] ?? [];
|
||||||
|
$lMime = $lMeta['mime'] ?? 'text/html';
|
||||||
|
$isPdf = ($lMime === 'application/pdf');
|
||||||
|
$isImg = str_starts_with($lMime, 'image/');
|
||||||
|
$lHost = parse_url($lnk['url'] ?? '', PHP_URL_HOST) ?? '';
|
||||||
|
?>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body p-3">
|
||||||
|
<div class="d-flex gap-3 align-items-start">
|
||||||
|
|
||||||
|
<!-- Vignette (locale uniquement pour respecter la CSP) -->
|
||||||
|
<?php if (!empty($lMeta['og_image']) && str_starts_with($lMeta['og_image'], '/')): ?>
|
||||||
|
<img src="<?= htmlspecialchars($lMeta['og_image']) ?>" alt=""
|
||||||
|
style="width:80px;height:60px;object-fit:cover;border-radius:4px;flex-shrink:0">
|
||||||
|
<?php else: ?>
|
||||||
|
<div style="width:40px;font-size:1.8rem;flex-shrink:0;text-align:center;padding-top:2px">
|
||||||
|
<?= $isPdf ? '📑' : ($isImg ? '🖼' : '🔗') ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div class="flex-grow-1 overflow-hidden">
|
||||||
|
<!-- Titre + URL -->
|
||||||
|
<div class="fw-semibold mb-1"><?= htmlspecialchars($lnk['name'] ?? '') ?></div>
|
||||||
|
<a href="<?= htmlspecialchars($lnk['url'] ?? '') ?>" target="_blank" rel="noopener"
|
||||||
|
class="small text-break font-monospace text-muted">
|
||||||
|
<?= htmlspecialchars($lnk['url'] ?? '') ?>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- Auteur · Date ajout -->
|
||||||
|
<div class="d-flex flex-wrap gap-3 mt-2 small">
|
||||||
|
<?php if (!empty($lnk['author'])): ?>
|
||||||
|
<span><span class="text-muted">Auteur :</span> <?= htmlspecialchars($lnk['author']) ?></span>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if (!empty($lnk['added_at'])): ?>
|
||||||
|
<span class="text-muted">Ajouté le <?= htmlspecialchars(date('d/m/Y à H:i', strtotime((string)$lnk['added_at']))) ?></span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Métadonnées -->
|
||||||
|
<?php
|
||||||
|
$visibleMeta = array_filter($lMeta, fn ($v, $k) => isset($metaLabels[$k]) && $v !== null && $v !== '' && $k !== 'height', ARRAY_FILTER_USE_BOTH);
|
||||||
|
?>
|
||||||
|
<?php if ($visibleMeta): ?>
|
||||||
|
<table class="table table-sm table-borderless mb-0 mt-2 small">
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ($metaLabels as $key => $label):
|
||||||
|
if (!isset($lMeta[$key]) || $lMeta[$key] === '' || $lMeta[$key] === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<tr>
|
||||||
|
<th class="text-muted fw-normal text-nowrap pe-3 align-top" style="width:140px"><?= $label ?></th>
|
||||||
|
<td><?= renderMetaCell($key, $lMeta[$key], $lMeta) ?></td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
// ── Pièces jointes ────────────────────────────────────────────────────────────
|
||||||
|
$filesMeta = $article['files_meta'] ?? [];
|
||||||
|
$coverFile = $article['cover'] ?? '';
|
||||||
|
$realFiles = array_values(array_filter($sourcesFiles, fn ($f) => !str_starts_with($f['name'], '_thumb_')));
|
||||||
|
?>
|
||||||
|
<section class="mb-5">
|
||||||
|
<h2 class="h5 border-bottom pb-2 mb-3">
|
||||||
|
Pièces jointes
|
||||||
|
<span class="badge bg-secondary fw-normal ms-1"><?= count($realFiles) ?></span>
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<?php if (empty($realFiles)): ?>
|
||||||
|
<p class="text-muted">Aucun fichier joint.</p>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="d-flex flex-column gap-3">
|
||||||
|
<?php foreach ($sourcesFiles as $f):
|
||||||
|
if (str_starts_with($f['name'], '_thumb_')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$fmeta = $filesMeta[$f['name']] ?? [];
|
||||||
|
$fExtra = $fmeta['meta'] ?? [];
|
||||||
|
$fileUrl = '/file?uuid=' . rawurlencode($article['uuid']) . '&name=' . rawurlencode($f['name']);
|
||||||
|
$isCover = ($f['name'] === $coverFile);
|
||||||
|
?>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body p-3">
|
||||||
|
<div class="d-flex gap-3 align-items-start">
|
||||||
|
|
||||||
|
<!-- Vignette -->
|
||||||
|
<?php if ($f['is_image']): ?>
|
||||||
|
<a href="<?= htmlspecialchars($fileUrl) ?>" target="_blank" rel="noopener" class="flex-shrink-0">
|
||||||
|
<img src="<?= htmlspecialchars($fileUrl) ?>" alt=""
|
||||||
|
style="width:80px;height:60px;object-fit:cover;border-radius:4px;<?= $isCover ? 'outline:2px solid #0d6efd' : '' ?>">
|
||||||
|
</a>
|
||||||
|
<?php else: ?>
|
||||||
|
<div style="width:40px;font-size:1.8rem;flex-shrink:0;text-align:center;padding-top:2px">
|
||||||
|
<?= match(true) {
|
||||||
|
str_starts_with($f['mime'], 'video/') => '🎬',
|
||||||
|
str_starts_with($f['mime'], 'audio/') => '🎵',
|
||||||
|
$f['mime'] === 'application/pdf' => '📑',
|
||||||
|
default => '📄',
|
||||||
|
} ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div class="flex-grow-1 overflow-hidden">
|
||||||
|
<!-- Titre + nom fichier -->
|
||||||
|
<div class="d-flex align-items-center gap-2 flex-wrap mb-1">
|
||||||
|
<a href="<?= htmlspecialchars($fileUrl) ?>" target="_blank" rel="noopener" class="fw-semibold">
|
||||||
|
<?= htmlspecialchars($fmeta['title'] ?? $f['name']) ?>
|
||||||
|
</a>
|
||||||
|
<?php if (!empty($fmeta['title'])): ?>
|
||||||
|
<code class="small text-muted"><?= htmlspecialchars($f['name']) ?></code>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if ($isCover): ?>
|
||||||
|
<span class="badge bg-primary">cover</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Auteur · Source · Taille -->
|
||||||
|
<div class="d-flex flex-wrap gap-3 small mb-1">
|
||||||
|
<span class="text-muted"><?= htmlspecialchars(number_format($f['size'] / 1024, 1)) ?> Ko</span>
|
||||||
|
<?php if (!empty($fmeta['author'])): ?>
|
||||||
|
<span><span class="text-muted">Auteur :</span> <?= htmlspecialchars($fmeta['author']) ?></span>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if (!empty($fmeta['source_url'])): ?>
|
||||||
|
<span>
|
||||||
|
<span class="text-muted">Source :</span>
|
||||||
|
<a href="<?= htmlspecialchars($fmeta['source_url']) ?>" target="_blank" rel="noopener"
|
||||||
|
class="font-monospace text-break small">
|
||||||
|
<?= htmlspecialchars($fmeta['source_url']) ?>
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Métadonnées EXIF/PDF -->
|
||||||
|
<?php
|
||||||
|
$visibleExtra = array_filter($fExtra, fn ($v, $k) => isset($metaLabels[$k]) && $v !== null && $v !== '' && $k !== 'height', ARRAY_FILTER_USE_BOTH);
|
||||||
|
?>
|
||||||
|
<?php if ($visibleExtra): ?>
|
||||||
|
<table class="table table-sm table-borderless mb-0 mt-1 small">
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ($metaLabels as $key => $label):
|
||||||
|
if (!isset($fExtra[$key]) || $fExtra[$key] === '' || $fExtra[$key] === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<tr>
|
||||||
|
<th class="text-muted fw-normal text-nowrap pe-3 align-top" style="width:140px"><?= $label ?></th>
|
||||||
|
<td><?= renderMetaCell($key, $fExtra[$key], $fExtra) ?></td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if (empty($fmeta) && empty($fExtra)): ?>
|
||||||
|
<span class="text-muted small">Pas de métadonnées enregistrées.</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
$content = ob_get_clean();
|
||||||
|
$title = 'Sources — ' . htmlspecialchars($article['title']);
|
||||||
|
include __DIR__ . '/layout.php';
|
||||||