This is an automated email from the ASF dual-hosted git repository.

sergehuber pushed a commit to branch UNOMI-932-unomi3-major-release-story
in repository https://gitbox.apache.org/repos/asf/unomi-site.git

commit 99ac7ead4297964cf79b3379d6b8b3972eba75a2
Author: Serge Huber <[email protected]>
AuthorDate: Wed Jul 15 09:50:09 2026 +0200

    Add Unomi 3 quality/ops blog wave with SVGs (UNOMI-932)
    
    Ship platform, OpenSearch, unit harness, shell, scheduler, tracing,
    OpenAPI, IT/manual/build.sh posts plus index/sitemap wiring and
    code-block contrast fixes.
---
 src/main/webapp/assets/css/unomi.css               |  24 ++-
 src/main/webapp/assets/images/unomi3/build-sh.svg  |  89 ++++++++
 .../images/unomi3/it-progress-before-after.svg     | 123 +++++++++++
 .../assets/images/unomi3/manual-plantuml-theme.svg |  94 +++++++++
 .../assets/images/unomi3/openapi-enrichment.svg    |  77 +++++++
 .../assets/images/unomi3/opensearch-dual.svg       |  67 ++++++
 .../assets/images/unomi3/platform-foundation.svg   |  73 +++++++
 .../assets/images/unomi3/request-tracing.svg       |  65 ++++++
 .../webapp/assets/images/unomi3/shell-commands.svg |  80 ++++++++
 .../webapp/assets/images/unomi3/task-scheduler.svg |  71 +++++++
 .../assets/images/unomi3/unit-harness-vs-it.svg    |  18 +-
 src/main/webapp/blog/index.html                    | 225 ++++++++++++++++++++-
 src/main/webapp/blog/unomi-3-build-sh.html         |  79 ++++++++
 src/main/webapp/blog/unomi-3-it-execution.html     |  96 +++++++++
 src/main/webapp/blog/unomi-3-manual.html           | 102 ++++++++++
 .../webapp/blog/unomi-3-openapi-enrichment.html    |  70 +++++++
 src/main/webapp/blog/unomi-3-opensearch.html       |  75 +++++++
 .../webapp/blog/unomi-3-platform-foundation.html   |  50 +++++
 src/main/webapp/blog/unomi-3-request-tracing.html  | 168 +++++++++++++++
 src/main/webapp/blog/unomi-3-shell-commands.html   | 116 +++++++++++
 src/main/webapp/blog/unomi-3-task-scheduler.html   | 102 ++++++++++
 .../webapp/blog/unomi-3-unit-testing-harness.html  |  84 ++++++++
 src/main/webapp/sitemap.xml                        |  50 +++++
 23 files changed, 1986 insertions(+), 12 deletions(-)

diff --git a/src/main/webapp/assets/css/unomi.css 
b/src/main/webapp/assets/css/unomi.css
index b1abb69..6e9de89 100644
--- a/src/main/webapp/assets/css/unomi.css
+++ b/src/main/webapp/assets/css/unomi.css
@@ -598,8 +598,8 @@ pre, code {
 }
 
 pre {
-    background: var(--unomi-text);
-    color: var(--unomi-border);
+    background: var(--unomi-text);           /* slate-800 */
+    color: #f1f5f9;                          /* high-contrast light slate */
     border-radius: var(--unomi-radius-sm);
     padding: 1.25rem;
     font-size: 0.85rem;
@@ -608,6 +608,26 @@ pre {
     -webkit-overflow-scrolling: touch;
 }
 
+pre code {
+    color: inherit;
+    background: transparent;
+    font-size: inherit;
+    padding: 0;
+}
+
+/* Light surface variant (opt-in) — keep text dark for contrast */
+pre.code-block-light,
+pre.bg-light {
+    background: var(--unomi-bg-soft) !important;
+    color: var(--unomi-text) !important;
+    border: 1px solid var(--unomi-border);
+}
+
+pre.code-block-light code,
+pre.bg-light code {
+    color: inherit;
+}
+
 code {
     font-size: 0.85em;
 }
diff --git a/src/main/webapp/assets/images/unomi3/build-sh.svg 
b/src/main/webapp/assets/images/unomi3/build-sh.svg
new file mode 100644
index 0000000..ff949f5
--- /dev/null
+++ b/src/main/webapp/assets/images/unomi3/build-sh.svg
@@ -0,0 +1,89 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg viewBox="0 0 840 380" fill="none" xmlns="http://www.w3.org/2000/svg"; 
role="img"
+     aria-label="Unomi build.sh: one entry point for preflight, Maven build, 
integration tests, and deploy">
+  <defs>
+<style>
+  svg {
+    --unomi-primary: #6d5ce7;
+    --unomi-primary-dark: #5a48c9;
+    --unomi-primary-light: #ede9fe;
+    --unomi-primary-subtle: #f5f3ff;
+    --unomi-accent: #00cec9;
+    --unomi-hero-accent: #a78bfa;
+    --unomi-text: #1e293b;
+    --unomi-text-muted: #64748b;
+    --unomi-white: #ffffff;
+    --unomi-bg-soft: #f8fafc;
+    --unomi-border: #e2e8f0;
+    --unomi-success: #059669;
+    --unomi-success-bg: #ecfdf5;
+    --unomi-amber: #d97706;
+    --unomi-amber-bg: #fef3c7;
+    --unomi-info: #2563eb;
+    --unomi-info-bg: #dbeafe;
+    --term-bg: #0f172a;
+    --term-ok: #34d399;
+    font-family: Inter, system-ui, sans-serif;
+  }
+  @keyframes dash-flow { to { stroke-dashoffset: -24; } }
+  @keyframes pulse-soft { 0%,100%{opacity:.55} 50%{opacity:.9} }
+  @keyframes float-y { 0%,100%{transform:translateY(0)} 
50%{transform:translateY(-3px)} }
+  @keyframes glow { 0%,100%{filter:drop-shadow(0 0 4px rgba(109,92,231,.35))} 
50%{filter:drop-shadow(0 0 10px rgba(109,92,231,.55))} }
+  .flow { stroke-dasharray: 6 5; animation: dash-flow 1.6s linear infinite; }
+  .pulse { animation: pulse-soft 3s ease-in-out infinite; }
+  .float { animation: float-y 3.2s ease-in-out infinite; }
+  .glow { animation: glow 3s ease-in-out infinite; }
+  @media (prefers-reduced-motion: reduce) {
+    .flow,.pulse,.float,.glow { animation: none !important; }
+  }
+</style>
+  </defs>
+
+  <rect width="840" height="380" rx="20" fill="var(--unomi-bg-soft)"/>
+  <text x="40" y="40" fill="var(--unomi-text)" font-size="18" 
font-weight="700">One build entry point</text>
+  <text x="40" y="62" fill="var(--unomi-text-muted)" font-size="13">./build.sh 
replaces scattered scripts | same path locally and in CI</text>
+
+  <rect x="300" y="100" width="240" height="72" rx="16" 
fill="var(--unomi-primary)" class="glow"/>
+  <text x="420" y="132" text-anchor="middle" fill="#ffffff" font-size="16" 
font-weight="700">./build.sh</text>
+  <text x="420" y="154" text-anchor="middle" fill="rgba(255,255,255,.85)" 
font-size="11">UNOMI-887 | build.ps1 twin</text>
+
+  <g class="float">
+    <rect x="40" y="210" width="170" height="120" rx="14" 
fill="var(--unomi-amber-bg)" stroke="var(--unomi-amber)" stroke-width="2"/>
+    <text x="125" y="242" text-anchor="middle" fill="var(--unomi-amber)" 
font-size="13" font-weight="700">Preflight</text>
+    <text x="125" y="268" text-anchor="middle" fill="var(--unomi-text)" 
font-size="11">Java | Maven | GraphViz</text>
+    <text x="125" y="290" text-anchor="middle" fill="var(--unomi-text-muted)" 
font-size="11">memory | disk | ports</text>
+    <text x="125" y="312" text-anchor="middle" fill="var(--unomi-text-muted)" 
font-size="11">clear failure output</text>
+  </g>
+
+  <g class="float" style="animation-delay:.3s">
+    <rect x="235" y="210" width="170" height="120" rx="14" 
fill="var(--unomi-info-bg)" stroke="var(--unomi-info)" stroke-width="2"/>
+    <text x="320" y="242" text-anchor="middle" fill="var(--unomi-info)" 
font-size="13" font-weight="700">Build</text>
+    <text x="320" y="268" text-anchor="middle" fill="var(--unomi-text)" 
font-size="11">install | -B -ntp in --ci</text>
+    <text x="320" y="290" text-anchor="middle" fill="var(--unomi-text-muted)" 
font-size="11">distributions | ES / OS</text>
+    <text x="320" y="312" text-anchor="middle" fill="var(--unomi-text-muted)" 
font-size="11">optional --javadoc</text>
+  </g>
+
+  <g class="float" style="animation-delay:.6s">
+    <rect x="430" y="210" width="170" height="120" rx="14" 
fill="var(--unomi-success-bg)" stroke="var(--unomi-success)" stroke-width="2"/>
+    <text x="515" y="242" text-anchor="middle" fill="var(--unomi-success)" 
font-size="13" font-weight="700">Test</text>
+    <text x="515" y="268" text-anchor="middle" fill="var(--unomi-text)" 
font-size="11">--integration-tests</text>
+    <text x="515" y="290" text-anchor="middle" fill="var(--unomi-text-muted)" 
font-size="11">--single-test | --it-debug</text>
+    <text x="515" y="312" text-anchor="middle" fill="var(--unomi-text-muted)" 
font-size="11">memory sampler</text>
+  </g>
+
+  <g class="float" style="animation-delay:.9s">
+    <rect x="625" y="210" width="175" height="120" rx="14" 
fill="var(--unomi-primary-light)" stroke="var(--unomi-primary)" 
stroke-width="2"/>
+    <text x="712" y="242" text-anchor="middle" 
fill="var(--unomi-primary-dark)" font-size="13" font-weight="700">Run / 
Deploy</text>
+    <text x="712" y="268" text-anchor="middle" fill="var(--unomi-text)" 
font-size="11">--deploy | --karaf-home</text>
+    <text x="712" y="290" text-anchor="middle" fill="var(--unomi-text-muted)" 
font-size="11">--debug | --auto-start</text>
+    <text x="712" y="312" text-anchor="middle" fill="var(--unomi-text-muted)" 
font-size="11">GitHub Actions --ci</text>
+  </g>
+
+  <path d="M340 172 L 125 210" stroke="var(--unomi-primary)" stroke-width="2" 
class="flow"/>
+  <path d="M390 172 L 320 210" stroke="var(--unomi-primary)" stroke-width="2" 
class="flow"/>
+  <path d="M450 172 L 515 210" stroke="var(--unomi-primary)" stroke-width="2" 
class="flow"/>
+  <path d="M500 172 L 712 210" stroke="var(--unomi-primary)" stroke-width="2" 
class="flow"/>
+
+  <rect x="40" y="345" width="760" height="22" rx="6" fill="var(--term-bg)"/>
+  <text x="56" y="360" fill="var(--term-ok)" font-size="11" 
font-family="ui-monospace, Menlo, monospace">$ ./build.sh --ci 
--integration-tests --use-opensearch</text>
+</svg>
diff --git a/src/main/webapp/assets/images/unomi3/it-progress-before-after.svg 
b/src/main/webapp/assets/images/unomi3/it-progress-before-after.svg
new file mode 100644
index 0000000..e7fb85e
--- /dev/null
+++ b/src/main/webapp/assets/images/unomi3/it-progress-before-after.svg
@@ -0,0 +1,123 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg viewBox="0 0 840 420" fill="none" xmlns="http://www.w3.org/2000/svg"; 
role="img"
+     aria-label="Before and after Unomi integration test console output: noisy 
downloads versus progress bar with ETA">
+  <defs>
+<style>
+  svg {
+    --unomi-primary: #6d5ce7;
+    --unomi-primary-dark: #5a48c9;
+    --unomi-primary-light: #ede9fe;
+    --unomi-primary-subtle: #f5f3ff;
+    --unomi-accent: #00cec9;
+    --unomi-hero-accent: #a78bfa;
+    --unomi-text: #1e293b;
+    --unomi-text-muted: #64748b;
+    --unomi-white: #ffffff;
+    --unomi-bg-soft: #f8fafc;
+    --unomi-border: #e2e8f0;
+    --unomi-success: #059669;
+    --unomi-success-bg: #ecfdf5;
+    --unomi-amber: #d97706;
+    --unomi-amber-bg: #fef3c7;
+    --unomi-danger: #dc2626;
+    --unomi-danger-bg: #fef2f2;
+    --unomi-info: #2563eb;
+    --unomi-info-bg: #dbeafe;
+    --term-bg: #0f172a;
+    --term-muted: #94a3b8;
+    --term-noise: #64748b;
+    --term-ok: #34d399;
+    --term-warn: #fbbf24;
+    font-family: Inter, system-ui, sans-serif;
+  }
+  @keyframes dash-flow { to { stroke-dashoffset: -24; } }
+  @keyframes pulse-soft { 0%,100%{opacity:.55} 50%{opacity:.9} }
+  @keyframes float-y { 0%,100%{transform:translateY(0)} 
50%{transform:translateY(-3px)} }
+  @keyframes bar-grow {
+    0% { width: 42px; }
+    50% { width: 118px; }
+    100% { width: 42px; }
+  }
+  @keyframes scroll-noise {
+    0% { transform: translateY(0); opacity: .9; }
+    100% { transform: translateY(-28px); opacity: .9; }
+  }
+  .flow { stroke-dasharray: 6 5; animation: dash-flow 1.6s linear infinite; }
+  .pulse { animation: pulse-soft 3s ease-in-out infinite; }
+  .float { animation: float-y 3.2s ease-in-out infinite; }
+  .bar-fill { animation: bar-grow 4.5s ease-in-out infinite; }
+  .noise-scroll { animation: scroll-noise 3.8s linear infinite; }
+  @media (prefers-reduced-motion: reduce) {
+    .flow,.pulse,.float,.bar-fill,.noise-scroll { animation: none !important; }
+  }
+</style>
+    <clipPath id="beforeClip">
+      <rect x="56" y="148" width="328" height="196" rx="8"/>
+    </clipPath>
+    <clipPath id="afterClip">
+      <rect x="456" y="148" width="328" height="196" rx="8"/>
+    </clipPath>
+  </defs>
+
+  <rect width="840" height="420" rx="20" fill="var(--unomi-bg-soft)"/>
+  <text x="40" y="40" fill="var(--unomi-text)" font-size="18" 
font-weight="700">IT execution you can read</text>
+  <text x="40" y="62" fill="var(--unomi-text-muted)" font-size="13">Progress 
bars, quieter Maven/Docker noise, and signal when a long suite is moving</text>
+
+  <g>
+    <rect x="40" y="92" width="360" height="280" rx="18" 
fill="var(--unomi-danger-bg)" stroke="var(--unomi-danger)" stroke-width="2"/>
+    <text x="60" y="122" fill="var(--unomi-danger)" font-size="14" 
font-weight="700">BEFORE</text>
+    <text x="140" y="122" fill="var(--unomi-text-muted)" 
font-size="12">transfer spam | no ETA</text>
+
+    <rect x="56" y="138" width="328" height="212" rx="12" 
fill="var(--term-bg)"/>
+    <circle cx="74" cy="154" r="4" fill="#f87171"/>
+    <circle cx="88" cy="154" r="4" fill="#fbbf24"/>
+    <circle cx="102" cy="154" r="4" fill="#4ade80"/>
+    <text x="118" y="158" fill="var(--term-muted)" font-size="10" 
font-family="ui-monospace, SFMono-Regular, Menlo, monospace">ci | itests 
(old)</text>
+
+    <g clip-path="url(#beforeClip)">
+      <g class="noise-scroll" font-family="ui-monospace, SFMono-Regular, 
Menlo, monospace" font-size="10">
+        <text x="68" y="178" fill="var(--term-noise)">Downloading from 
central: junit.jar</text>
+        <text x="68" y="194" fill="var(--term-noise)">Progress (1): 4.1/384 
kB</text>
+        <text x="68" y="210" fill="var(--term-noise)">Progress (1): 48/384 
kB</text>
+        <text x="68" y="226" fill="var(--term-noise)">Progress (2): 128 kB | 
3.2/512 kB</text>
+        <text x="68" y="242" fill="var(--term-noise)">Downloaded from central: 
junit.jar</text>
+        <text x="68" y="258" fill="var(--term-noise)">Downloading from 
central: hamcrest.jar</text>
+        <text x="68" y="274" fill="var(--term-noise)">Progress (1): 7.2/45 
kB</text>
+        <text x="68" y="290" fill="var(--term-noise)">[INFO] --- docker:start 
(elasticsearch)</text>
+        <text x="68" y="306" fill="var(--term-noise)">... thousands of 
identical lines ...</text>
+        <text x="68" y="322" fill="var(--term-noise)">Downloading from 
central: bundle.jar</text>
+        <text x="68" y="338" fill="var(--term-noise)">Progress (1): 16/902 
kB</text>
+        <text x="68" y="354" fill="var(--term-muted)">Tests run: ???  ETA: 
unknown</text>
+      </g>
+    </g>
+  </g>
+
+  <g>
+    <rect x="440" y="92" width="360" height="280" rx="18" 
fill="var(--unomi-success-bg)" stroke="var(--unomi-success)" stroke-width="2"/>
+    <text x="460" y="122" fill="var(--unomi-success)" font-size="14" 
font-weight="700">AFTER</text>
+    <text x="528" y="122" fill="var(--unomi-text-muted)" 
font-size="12">progress | ETA | slowest CSV</text>
+
+    <rect x="456" y="138" width="328" height="212" rx="12" 
fill="var(--term-bg)"/>
+    <circle cx="474" cy="154" r="4" fill="#f87171"/>
+    <circle cx="488" cy="154" r="4" fill="#fbbf24"/>
+    <circle cx="502" cy="154" r="4" fill="#4ade80"/>
+    <text x="518" y="158" fill="var(--term-muted)" font-size="10" 
font-family="ui-monospace, SFMono-Regular, Menlo, monospace">ci | itests (Unomi 
3)</text>
+
+    <g clip-path="url(#afterClip)" font-family="ui-monospace, SFMono-Regular, 
Menlo, monospace" font-size="10">
+      <text x="468" y="182" fill="var(--unomi-accent)">Apache Unomi  |  
Integration tests</text>
+      <text x="468" y="200" fill="var(--term-muted)">Total tests: 412  |  
Search: Elasticsearch</text>
+
+      <rect x="468" y="216" width="304" height="28" rx="6" fill="#1e293b" 
stroke="#334155"/>
+      <rect x="472" y="220" width="118" height="20" rx="4" 
fill="var(--term-ok)" class="bar-fill float"/>
+      <text x="478" y="234" fill="#0f172a" font-size="9" 
font-weight="700">38%</text>
+
+      <text x="468" y="268" fill="var(--term-ok)">Progress: 38.11% 
(157/412)</text>
+      <text x="468" y="284" fill="var(--term-warn)">ETA: 18m 42s  |  OK: 157  
Failed: 0</text>
+      <text x="468" y="310" fill="var(--term-muted)">Maven: -B -ntp  |  Docker 
logs to files</text>
+      <text x="468" y="326" fill="var(--unomi-hero-accent)">Top 10 slowest - 
CSV at finish</text>
+    </g>
+  </g>
+
+  <path d="M 408 232 L 432 232" stroke="var(--unomi-primary)" 
stroke-width="2.5" stroke-linecap="round" class="flow"/>
+  <path d="M 424 224 L 436 232 L 424 240" fill="var(--unomi-primary)" 
class="pulse"/>
+</svg>
diff --git a/src/main/webapp/assets/images/unomi3/manual-plantuml-theme.svg 
b/src/main/webapp/assets/images/unomi3/manual-plantuml-theme.svg
new file mode 100644
index 0000000..eae593c
--- /dev/null
+++ b/src/main/webapp/assets/images/unomi3/manual-plantuml-theme.svg
@@ -0,0 +1,94 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg viewBox="0 0 840 400" fill="none" xmlns="http://www.w3.org/2000/svg"; 
role="img"
+     aria-label="Unomi 3 manual: AsciiDoc with PlantUML sources builds into a 
branded HTML manual with diagrams">
+  <defs>
+<style>
+  svg {
+    --unomi-primary: #6d5ce7;
+    --unomi-primary-dark: #5a48c9;
+    --unomi-primary-light: #ede9fe;
+    --unomi-primary-subtle: #f5f3ff;
+    --unomi-accent: #00cec9;
+    --unomi-hero-accent: #a78bfa;
+    --unomi-text: #1e293b;
+    --unomi-text-muted: #64748b;
+    --unomi-white: #ffffff;
+    --unomi-bg-soft: #f8fafc;
+    --unomi-border: #e2e8f0;
+    --unomi-success: #059669;
+    --unomi-success-bg: #ecfdf5;
+    --unomi-info: #2563eb;
+    --unomi-info-bg: #dbeafe;
+    --term-bg: #0f172a;
+    --term-muted: #94a3b8;
+    --term-ok: #34d399;
+    font-family: Inter, system-ui, sans-serif;
+  }
+  @keyframes dash-flow { to { stroke-dashoffset: -24; } }
+  @keyframes pulse-soft { 0%,100%{opacity:.55} 50%{opacity:.9} }
+  @keyframes float-y { 0%,100%{transform:translateY(0)} 
50%{transform:translateY(-3px)} }
+  @keyframes glow { 0%,100%{filter:drop-shadow(0 0 4px rgba(109,92,231,.35))} 
50%{filter:drop-shadow(0 0 10px rgba(109,92,231,.55))} }
+  .flow { stroke-dasharray: 6 5; animation: dash-flow 1.6s linear infinite; }
+  .pulse { animation: pulse-soft 3s ease-in-out infinite; }
+  .float { animation: float-y 3.2s ease-in-out infinite; }
+  .glow { animation: glow 3s ease-in-out infinite; }
+  @media (prefers-reduced-motion: reduce) {
+    .flow,.pulse,.float,.glow { animation: none !important; }
+  }
+</style>
+  </defs>
+
+  <rect width="840" height="400" rx="20" fill="var(--unomi-bg-soft)"/>
+  <text x="40" y="40" fill="var(--unomi-text)" font-size="18" 
font-weight="700">Manual as code</text>
+  <text x="40" y="62" fill="var(--unomi-text-muted)" font-size="13">PlantUML 
in AsciiDoc | branded HTML/PDF | diagrams rebuild with the docs</text>
+
+  <g>
+    <rect x="40" y="88" width="310" height="270" rx="18" 
fill="var(--unomi-info-bg)" stroke="var(--unomi-info)" stroke-width="2"/>
+    <text x="60" y="118" fill="var(--unomi-info)" font-size="14" 
font-weight="700">AsciiDoc source</text>
+    <rect x="56" y="136" width="278" height="200" rx="12" 
fill="var(--term-bg)"/>
+    <text x="72" y="162" fill="var(--unomi-accent)" font-size="11" 
font-family="ui-monospace, Menlo, monospace">[plantuml]</text>
+    <text x="72" y="182" fill="var(--term-muted)" font-size="10" 
font-family="ui-monospace, Menlo, monospace">----</text>
+    <text x="72" y="202" fill="var(--term-ok)" font-size="10" 
font-family="ui-monospace, Menlo, monospace">@startuml</text>
+    <text x="72" y="220" fill="#e2e8f0" font-size="10" 
font-family="ui-monospace, Menlo, monospace">!include C4_Component.puml</text>
+    <text x="72" y="238" fill="#e2e8f0" font-size="10" 
font-family="ui-monospace, Menlo, monospace">Container(unomi, "Unomi")</text>
+    <text x="72" y="256" fill="#e2e8f0" font-size="10" 
font-family="ui-monospace, Menlo, monospace">Component(rest, "REST")</text>
+    <text x="72" y="274" fill="var(--term-ok)" font-size="10" 
font-family="ui-monospace, Menlo, monospace">@enduml</text>
+    <text x="72" y="294" fill="var(--term-muted)" font-size="10" 
font-family="ui-monospace, Menlo, monospace">----</text>
+    <text x="72" y="318" fill="var(--unomi-hero-accent)" font-size="11" 
font-family="ui-monospace, Menlo, monospace">+ GraphViz | reviewable 
diffs</text>
+  </g>
+
+  <g>
+    <path d="M 368 220 L 412 220" stroke="var(--unomi-primary)" 
stroke-width="2.5" stroke-linecap="round" class="flow"/>
+    <path d="M 404 212 L 416 220 L 404 228" fill="var(--unomi-primary)" 
class="pulse"/>
+    <text x="390" y="204" text-anchor="middle" 
fill="var(--unomi-primary-dark)" font-size="11" font-weight="600">build</text>
+  </g>
+
+  <g class="float">
+    <rect x="430" y="88" width="370" height="270" rx="18" 
fill="var(--unomi-primary-light)" stroke="var(--unomi-primary)" 
stroke-width="2" class="glow"/>
+    <text x="450" y="118" fill="var(--unomi-primary-dark)" font-size="14" 
font-weight="700">Branded manual</text>
+    <rect x="450" y="136" width="330" height="198" rx="12" 
fill="var(--unomi-white)" stroke="var(--unomi-border)"/>
+    <rect x="450" y="136" width="330" height="36" rx="12" 
fill="var(--unomi-primary)"/>
+    <rect x="450" y="158" width="330" height="14" fill="var(--unomi-primary)"/>
+    <text x="468" y="158" fill="#ffffff" font-size="11" 
font-weight="600">Apache Unomi 3.x | Documentation</text>
+    <rect x="458" y="184" width="88" height="140" rx="8" 
fill="var(--unomi-primary-subtle)"/>
+    <text x="468" y="204" fill="var(--unomi-primary-dark)" font-size="9" 
font-weight="700">Contents</text>
+    <text x="468" y="222" fill="var(--unomi-text-muted)" font-size="8">What's 
new</text>
+    <text x="468" y="238" fill="var(--unomi-text)" font-size="8" 
font-weight="600">Architecture</text>
+    <text x="468" y="254" fill="var(--unomi-text-muted)" 
font-size="8">Migrations</text>
+    <text x="468" y="270" fill="var(--unomi-text-muted)" 
font-size="8">Scheduler</text>
+    <text x="468" y="286" fill="var(--unomi-text-muted)" 
font-size="8">Security</text>
+    <rect x="560" y="184" width="100" height="64" rx="8" 
fill="var(--unomi-success-bg)" stroke="var(--unomi-success)"/>
+    <circle cx="590" cy="208" r="10" fill="var(--unomi-success)" 
opacity=".85"/>
+    <rect x="610" y="200" width="36" height="10" rx="3" 
fill="var(--unomi-success)" opacity=".5"/>
+    <rect x="610" y="216" width="28" height="10" rx="3" 
fill="var(--unomi-success)" opacity=".35"/>
+    <text x="610" y="238" fill="var(--unomi-success)" font-size="8" 
font-weight="600">C4 | PNG</text>
+    <rect x="672" y="184" width="96" height="64" rx="8" 
fill="var(--unomi-info-bg)" stroke="var(--unomi-info)"/>
+    <path d="M690 208 H 748 M 710 196 V 232" stroke="var(--unomi-info)" 
stroke-width="2" class="flow"/>
+    <circle cx="700" cy="208" r="5" fill="var(--unomi-info)"/>
+    <circle cx="748" cy="208" r="5" fill="var(--unomi-accent)"/>
+    <text x="700" y="238" fill="var(--unomi-info)" font-size="8" 
font-weight="600">sequence</text>
+    <text x="560" y="278" fill="var(--unomi-text)" font-size="11" 
font-weight="600">Inter | brand tokens</text>
+    <text x="560" y="296" fill="var(--unomi-text-muted)" 
font-size="10">Responsive nav + TOC | PDF theme</text>
+    <text x="560" y="314" fill="var(--unomi-primary-dark)" font-size="10" 
font-weight="600">62 PlantUML diagrams | text in git</text>
+  </g>
+</svg>
diff --git a/src/main/webapp/assets/images/unomi3/openapi-enrichment.svg 
b/src/main/webapp/assets/images/unomi3/openapi-enrichment.svg
new file mode 100644
index 0000000..d11d93a
--- /dev/null
+++ b/src/main/webapp/assets/images/unomi3/openapi-enrichment.svg
@@ -0,0 +1,77 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg viewBox="0 0 840 400" fill="none" xmlns="http://www.w3.org/2000/svg"; 
role="img"
+     aria-label="Unomi 3.1 OpenAPI enrichment: Javadoc packs merge into CXF 
openapi.json">
+  <defs>
+<style>
+  svg {
+    --unomi-primary: #6d5ce7;
+    --unomi-primary-dark: #5a48c9;
+    --unomi-primary-light: #ede9fe;
+    --unomi-primary-subtle: #f5f3ff;
+    --unomi-accent: #00cec9;
+    --unomi-hero-accent: #a78bfa;
+    --unomi-text: #1e293b;
+    --unomi-text-muted: #64748b;
+    --unomi-white: #ffffff;
+    --unomi-bg-soft: #f8fafc;
+    --unomi-border: #e2e8f0;
+    --unomi-success: #059669;
+    --unomi-success-bg: #ecfdf5;
+    --unomi-amber: #d97706;
+    --unomi-amber-bg: #fef3c7;
+    --unomi-info: #2563eb;
+    --unomi-info-bg: #dbeafe;
+    font-family: Inter, system-ui, sans-serif;
+  }
+  @keyframes dash-flow { to { stroke-dashoffset: -24; } }
+  @keyframes pulse-soft { 0%,100%{opacity:.55} 50%{opacity:.9} }
+  @keyframes float-y { 0%,100%{transform:translateY(0)} 
50%{transform:translateY(-3px)} }
+  @keyframes glow { 0%,100%{filter:drop-shadow(0 0 4px rgba(109,92,231,.35))} 
50%{filter:drop-shadow(0 0 10px rgba(109,92,231,.55))} }
+  .flow { stroke-dasharray: 6 5; animation: dash-flow 1.6s linear infinite; }
+  .pulse { animation: pulse-soft 3s ease-in-out infinite; }
+  .float { animation: float-y 3.2s ease-in-out infinite; }
+  .glow { animation: glow 3s ease-in-out infinite; }
+  @media (prefers-reduced-motion: reduce) {
+    .flow,.pulse,.float,.glow { animation: none !important; }
+  }
+</style>
+  </defs>
+  <rect width="840" height="400" rx="20" fill="var(--unomi-bg-soft)"/>
+  <text x="40" y="40" fill="var(--unomi-text)" font-size="18" 
font-weight="700">Richer OpenAPI from Javadoc</text>
+  <text x="40" y="62" fill="var(--unomi-text-muted)" font-size="13">Unomi 3.1: 
build-time packs + runtime CXF merge (UNOMI-963)</text>
+
+  <g class="float">
+    <rect x="40" y="100" width="220" height="240" rx="14" 
fill="var(--unomi-amber-bg)" stroke="var(--unomi-amber)" stroke-width="2"/>
+    <text x="150" y="140" text-anchor="middle" fill="var(--unomi-amber)" 
font-size="13" font-weight="700">1. Sources</text>
+    <text x="150" y="172" text-anchor="middle" fill="var(--unomi-text)" 
font-size="12">JAX-RS endpoints</text>
+    <text x="150" y="198" text-anchor="middle" fill="var(--unomi-text)" 
font-size="12">@param / @return</text>
+    <text x="150" y="224" text-anchor="middle" fill="var(--unomi-text)" 
font-size="12">@api.status</text>
+    <text x="150" y="250" text-anchor="middle" fill="var(--unomi-text)" 
font-size="12">@api.example</text>
+    <text x="150" y="290" text-anchor="middle" fill="var(--unomi-text-muted)" 
font-size="11">rest + extensions</text>
+  </g>
+
+  <path d="M275 220 H325" stroke="var(--unomi-primary)" stroke-width="3" 
class="flow"/>
+
+  <g class="float" style="animation-delay:.3s">
+    <rect x="335" y="100" width="170" height="240" rx="14" 
fill="var(--unomi-primary)" class="glow"/>
+    <text x="420" y="160" text-anchor="middle" fill="#fff" font-size="13" 
font-weight="700">Maven Mojo</text>
+    <text x="420" y="190" text-anchor="middle" fill="rgba(255,255,255,.9)" 
font-size="11">enrichment pack</text>
+    <text x="420" y="220" text-anchor="middle" fill="rgba(255,255,255,.9)" 
font-size="11">META-INF/...</text>
+    <text x="420" y="250" text-anchor="middle" fill="rgba(255,255,255,.9)" 
font-size="11">openapi-doc.json</text>
+    <text x="420" y="300" text-anchor="middle" fill="rgba(255,255,255,.75)" 
font-size="11">per module JAR</text>
+  </g>
+
+  <path d="M520 220 H570" stroke="var(--unomi-primary)" stroke-width="3" 
class="flow"/>
+
+  <g class="float" style="animation-delay:.6s">
+    <rect x="580" y="100" width="220" height="240" rx="14" 
fill="var(--unomi-success-bg)" stroke="var(--unomi-success)" stroke-width="2"/>
+    <text x="690" y="140" text-anchor="middle" fill="var(--unomi-success)" 
font-size="13" font-weight="700">2. Runtime</text>
+    <text x="690" y="172" text-anchor="middle" fill="var(--unomi-text)" 
font-size="12">CXF OpenApiFeature</text>
+    <text x="690" y="198" text-anchor="middle" fill="var(--unomi-text)" 
font-size="12">merge all packs</text>
+    <text x="690" y="224" text-anchor="middle" fill="var(--unomi-text)" 
font-size="12">/cxs/openapi.json</text>
+    <text x="690" y="262" text-anchor="middle" fill="var(--unomi-text-muted)" 
font-size="11">summaries | statuses</text>
+    <text x="690" y="286" text-anchor="middle" fill="var(--unomi-text-muted)" 
font-size="11">examples | schemas</text>
+    <text x="690" y="318" text-anchor="middle" 
fill="var(--unomi-primary-dark)" font-size="12" font-weight="600">Agent / IDE 
friendly</text>
+  </g>
+
+</svg>
diff --git a/src/main/webapp/assets/images/unomi3/opensearch-dual.svg 
b/src/main/webapp/assets/images/unomi3/opensearch-dual.svg
new file mode 100644
index 0000000..75321d9
--- /dev/null
+++ b/src/main/webapp/assets/images/unomi3/opensearch-dual.svg
@@ -0,0 +1,67 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg viewBox="0 0 840 380" fill="none" xmlns="http://www.w3.org/2000/svg"; 
role="img"
+     aria-label="Unomi 3: Elasticsearch 9 and OpenSearch as dual persistence 
backends">
+  <defs>
+<style>
+  svg {
+    --unomi-primary: #6d5ce7;
+    --unomi-primary-dark: #5a48c9;
+    --unomi-primary-light: #ede9fe;
+    --unomi-primary-subtle: #f5f3ff;
+    --unomi-accent: #00cec9;
+    --unomi-hero-accent: #a78bfa;
+    --unomi-text: #1e293b;
+    --unomi-text-muted: #64748b;
+    --unomi-white: #ffffff;
+    --unomi-bg-soft: #f8fafc;
+    --unomi-border: #e2e8f0;
+    --unomi-success: #059669;
+    --unomi-success-bg: #ecfdf5;
+    --unomi-amber: #d97706;
+    --unomi-amber-bg: #fef3c7;
+    --unomi-info: #2563eb;
+    --unomi-info-bg: #dbeafe;
+    font-family: Inter, system-ui, sans-serif;
+  }
+  @keyframes dash-flow { to { stroke-dashoffset: -24; } }
+  @keyframes pulse-soft { 0%,100%{opacity:.55} 50%{opacity:.9} }
+  @keyframes float-y { 0%,100%{transform:translateY(0)} 
50%{transform:translateY(-3px)} }
+  @keyframes glow { 0%,100%{filter:drop-shadow(0 0 4px rgba(109,92,231,.35))} 
50%{filter:drop-shadow(0 0 10px rgba(109,92,231,.55))} }
+  .flow { stroke-dasharray: 6 5; animation: dash-flow 1.6s linear infinite; }
+  .pulse { animation: pulse-soft 3s ease-in-out infinite; }
+  .float { animation: float-y 3.2s ease-in-out infinite; }
+  .glow { animation: glow 3s ease-in-out infinite; }
+  @media (prefers-reduced-motion: reduce) {
+    .flow,.pulse,.float,.glow { animation: none !important; }
+  }
+</style>
+  </defs>
+  <rect width="840" height="380" rx="20" fill="var(--unomi-bg-soft)"/>
+  <text x="40" y="40" fill="var(--unomi-text)" font-size="18" 
font-weight="700">One CDP, two search backends</text>
+  <text x="40" y="62" fill="var(--unomi-text-muted)" font-size="13">Unomi 3 
ships Elasticsearch 9 and first-class OpenSearch distributions</text>
+
+  <g class="float">
+    <rect x="40" y="100" width="340" height="220" rx="16" 
fill="var(--unomi-info-bg)" stroke="var(--unomi-info)" stroke-width="2"/>
+    <text x="210" y="140" text-anchor="middle" fill="var(--unomi-info)" 
font-size="15" font-weight="700">Elasticsearch 9</text>
+    <text x="210" y="168" text-anchor="middle" fill="var(--unomi-text)" 
font-size="12">persistence-elasticsearch</text>
+    <text x="210" y="196" text-anchor="middle" fill="var(--unomi-text-muted)" 
font-size="12">Java API Client (no HLRC)</text>
+    <text x="210" y="220" text-anchor="middle" fill="var(--unomi-text-muted)" 
font-size="12">org.apache.unomi.elasticsearch.*</text>
+    <text x="210" y="252" text-anchor="middle" fill="var(--unomi-text)" 
font-size="12" font-weight="600">Default distribution</text>
+    <text x="210" y="280" text-anchor="middle" fill="var(--unomi-text-muted)" 
font-size="11">Pin e.g. 9.4.x in 3.1</text>
+  </g>
+
+  <g class="float" style="animation-delay:.4s">
+    <rect x="460" y="100" width="340" height="220" rx="16" 
fill="var(--unomi-success-bg)" stroke="var(--unomi-success)" stroke-width="2"/>
+    <text x="630" y="140" text-anchor="middle" fill="var(--unomi-success)" 
font-size="15" font-weight="700">OpenSearch</text>
+    <text x="630" y="168" text-anchor="middle" fill="var(--unomi-text)" 
font-size="12">persistence-opensearch</text>
+    <text x="630" y="196" text-anchor="middle" fill="var(--unomi-text-muted)" 
font-size="12">Same PersistenceService SPI</text>
+    <text x="630" y="220" text-anchor="middle" fill="var(--unomi-text-muted)" 
font-size="12">org.apache.unomi.opensearch.*</text>
+    <text x="630" y="252" text-anchor="middle" fill="var(--unomi-text)" 
font-size="12" font-weight="600">-Duse.opensearch=true</text>
+    <text x="630" y="280" text-anchor="middle" fill="var(--unomi-text-muted)" 
font-size="11">Migrate guide in the manual</text>
+  </g>
+
+  <path d="M390 210 H450" stroke="var(--unomi-primary)" stroke-width="3" 
class="flow"/>
+  <circle cx="420" cy="210" r="18" fill="var(--unomi-primary)" class="glow"/>
+  <text x="420" y="215" text-anchor="middle" fill="#fff" font-size="10" 
font-weight="700">SPI</text>
+
+</svg>
diff --git a/src/main/webapp/assets/images/unomi3/platform-foundation.svg 
b/src/main/webapp/assets/images/unomi3/platform-foundation.svg
new file mode 100644
index 0000000..b20f50b
--- /dev/null
+++ b/src/main/webapp/assets/images/unomi3/platform-foundation.svg
@@ -0,0 +1,73 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg viewBox="0 0 840 400" fill="none" xmlns="http://www.w3.org/2000/svg"; 
role="img"
+     aria-label="Unomi 3 platform foundation: Java 17, Karaf 4.4, ES9, 
persistence clustering">
+  <defs>
+<style>
+  svg {
+    --unomi-primary: #6d5ce7;
+    --unomi-primary-dark: #5a48c9;
+    --unomi-primary-light: #ede9fe;
+    --unomi-primary-subtle: #f5f3ff;
+    --unomi-accent: #00cec9;
+    --unomi-hero-accent: #a78bfa;
+    --unomi-text: #1e293b;
+    --unomi-text-muted: #64748b;
+    --unomi-white: #ffffff;
+    --unomi-bg-soft: #f8fafc;
+    --unomi-border: #e2e8f0;
+    --unomi-success: #059669;
+    --unomi-success-bg: #ecfdf5;
+    --unomi-amber: #d97706;
+    --unomi-amber-bg: #fef3c7;
+    --unomi-info: #2563eb;
+    --unomi-info-bg: #dbeafe;
+    font-family: Inter, system-ui, sans-serif;
+  }
+  @keyframes dash-flow { to { stroke-dashoffset: -24; } }
+  @keyframes pulse-soft { 0%,100%{opacity:.55} 50%{opacity:.9} }
+  @keyframes float-y { 0%,100%{transform:translateY(0)} 
50%{transform:translateY(-3px)} }
+  @keyframes glow { 0%,100%{filter:drop-shadow(0 0 4px rgba(109,92,231,.35))} 
50%{filter:drop-shadow(0 0 10px rgba(109,92,231,.55))} }
+  .flow { stroke-dasharray: 6 5; animation: dash-flow 1.6s linear infinite; }
+  .pulse { animation: pulse-soft 3s ease-in-out infinite; }
+  .float { animation: float-y 3.2s ease-in-out infinite; }
+  .glow { animation: glow 3s ease-in-out infinite; }
+  @media (prefers-reduced-motion: reduce) {
+    .flow,.pulse,.float,.glow { animation: none !important; }
+  }
+</style>
+  </defs>
+  <rect width="840" height="400" rx="20" fill="var(--unomi-bg-soft)"/>
+  <text x="40" y="40" fill="var(--unomi-text)" font-size="18" 
font-weight="700">Platform foundation (3.0 floor)</text>
+  <text x="40" y="62" fill="var(--unomi-text-muted)" font-size="13">The 
runtime that makes multi-tenancy and ops tooling possible</text>
+
+  <g class="float">
+    <rect x="40" y="100" width="175" height="120" rx="14" 
fill="var(--unomi-primary-light)" stroke="var(--unomi-primary)" 
stroke-width="2"/>
+    <text x="127" y="145" text-anchor="middle" 
fill="var(--unomi-primary-dark)" font-size="14" font-weight="700">Java 
17+</text>
+    <text x="127" y="172" text-anchor="middle" fill="var(--unomi-text-muted)" 
font-size="11">Java 11 retired</text>
+    <text x="127" y="196" text-anchor="middle" fill="var(--unomi-text-muted)" 
font-size="11">installer baseline</text>
+  </g>
+  <g class="float" style="animation-delay:.2s">
+    <rect x="235" y="100" width="175" height="120" rx="14" 
fill="var(--unomi-info-bg)" stroke="var(--unomi-info)" stroke-width="2"/>
+    <text x="322" y="145" text-anchor="middle" fill="var(--unomi-info)" 
font-size="14" font-weight="700">Karaf 4.4</text>
+    <text x="322" y="172" text-anchor="middle" fill="var(--unomi-text-muted)" 
font-size="11">OSGi runtime</text>
+    <text x="322" y="196" text-anchor="middle" fill="var(--unomi-text-muted)" 
font-size="11">4.4.11 in 3.1</text>
+  </g>
+  <g class="float" style="animation-delay:.4s">
+    <rect x="430" y="100" width="175" height="120" rx="14" 
fill="var(--unomi-amber-bg)" stroke="var(--unomi-amber)" stroke-width="2"/>
+    <text x="517" y="145" text-anchor="middle" fill="var(--unomi-amber)" 
font-size="14" font-weight="700">Search 9.x</text>
+    <text x="517" y="172" text-anchor="middle" fill="var(--unomi-text-muted)" 
font-size="11">ES Java API client</text>
+    <text x="517" y="196" text-anchor="middle" fill="var(--unomi-text-muted)" 
font-size="11">OpenSearch option</text>
+  </g>
+  <g class="float" style="animation-delay:.6s">
+    <rect x="625" y="100" width="175" height="120" rx="14" 
fill="var(--unomi-success-bg)" stroke="var(--unomi-success)" stroke-width="2"/>
+    <text x="712" y="145" text-anchor="middle" fill="var(--unomi-success)" 
font-size="14" font-weight="700">Clustering</text>
+    <text x="712" y="172" text-anchor="middle" fill="var(--unomi-text-muted)" 
font-size="11">No Cellar/Hazelcast</text>
+    <text x="712" y="196" text-anchor="middle" fill="var(--unomi-text-muted)" 
font-size="11">index heartbeats</text>
+  </g>
+
+  <rect x="40" y="250" width="760" height="110" rx="16" 
fill="var(--unomi-white)" stroke="var(--unomi-border)" stroke-width="2"/>
+  <text x="60" y="290" fill="var(--unomi-primary-dark)" font-size="14" 
font-weight="700">Persistence-based cluster nodes</text>
+  <text x="60" y="318" fill="var(--unomi-text)" font-size="12">ClusterNode 
docs in Elasticsearch / OpenSearch | heartbeats + stats | peer discovery via 
shared index</text>
+  <text x="60" y="342" fill="var(--unomi-text-muted)" font-size="12">UNOMI-877 
| also powers cluster-aware task scheduler locks</text>
+
+</svg>
diff --git a/src/main/webapp/assets/images/unomi3/request-tracing.svg 
b/src/main/webapp/assets/images/unomi3/request-tracing.svg
new file mode 100644
index 0000000..66171b4
--- /dev/null
+++ b/src/main/webapp/assets/images/unomi3/request-tracing.svg
@@ -0,0 +1,65 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg viewBox="0 0 840 400" fill="none" xmlns="http://www.w3.org/2000/svg"; 
role="img"
+     aria-label="Unomi request tracing: explain=true returns a requestTracing 
tree">
+  <defs>
+<style>
+  svg {
+    --unomi-primary: #6d5ce7;
+    --unomi-primary-dark: #5a48c9;
+    --unomi-primary-light: #ede9fe;
+    --unomi-primary-subtle: #f5f3ff;
+    --unomi-accent: #00cec9;
+    --unomi-hero-accent: #a78bfa;
+    --unomi-text: #1e293b;
+    --unomi-text-muted: #64748b;
+    --unomi-white: #ffffff;
+    --unomi-bg-soft: #f8fafc;
+    --unomi-border: #e2e8f0;
+    --unomi-success: #059669;
+    --unomi-success-bg: #ecfdf5;
+    --unomi-amber: #d97706;
+    --unomi-amber-bg: #fef3c7;
+    --unomi-info: #2563eb;
+    --unomi-info-bg: #dbeafe;
+    font-family: Inter, system-ui, sans-serif;
+  }
+  @keyframes dash-flow { to { stroke-dashoffset: -24; } }
+  @keyframes pulse-soft { 0%,100%{opacity:.55} 50%{opacity:.9} }
+  @keyframes float-y { 0%,100%{transform:translateY(0)} 
50%{transform:translateY(-3px)} }
+  @keyframes glow { 0%,100%{filter:drop-shadow(0 0 4px rgba(109,92,231,.35))} 
50%{filter:drop-shadow(0 0 10px rgba(109,92,231,.55))} }
+  .flow { stroke-dasharray: 6 5; animation: dash-flow 1.6s linear infinite; }
+  .pulse { animation: pulse-soft 3s ease-in-out infinite; }
+  .float { animation: float-y 3.2s ease-in-out infinite; }
+  .glow { animation: glow 3s ease-in-out infinite; }
+  @media (prefers-reduced-motion: reduce) {
+    .flow,.pulse,.float,.glow { animation: none !important; }
+  }
+</style>
+  </defs>
+  <rect width="840" height="400" rx="20" fill="var(--unomi-bg-soft)"/>
+  <text x="40" y="40" fill="var(--unomi-text)" font-size="18" 
font-weight="700">Request tracing with explain=true</text>
+  <text x="40" y="62" fill="var(--unomi-text-muted)" font-size="13">Admin / 
tenant-admin only | tree of conditions, rules, and actions</text>
+
+  <rect x="40" y="95" width="280" height="260" rx="16" 
fill="var(--unomi-white)" stroke="var(--unomi-border)" stroke-width="2"/>
+  <text x="60" y="128" fill="var(--unomi-primary-dark)" font-size="13" 
font-weight="700">Your request</text>
+  <text x="60" y="156" fill="var(--unomi-text)" font-size="12">POST 
/cxs/context.json</text>
+  <text x="60" y="178" fill="var(--unomi-text)" font-size="12">POST 
/cxs/eventcollector</text>
+  <text x="60" y="210" fill="var(--unomi-accent)" font-size="12" 
font-weight="700">?explain=true</text>
+  <text x="60" y="242" fill="var(--unomi-text-muted)" font-size="11">Auth: 
tenantId:privateKey</text>
+  <text x="60" y="266" fill="var(--unomi-text-muted)" font-size="11">(Basic) 
TENANT_ADMIN</text>
+  <text x="60" y="300" fill="var(--unomi-amber)" font-size="11" 
font-weight="600">Public key alone =&gt; 403</text>
+  <text x="60" y="324" fill="var(--unomi-text-muted)" 
font-size="11">UNOMI-873</text>
+
+  <path d="M340 220 H400" stroke="var(--unomi-primary)" stroke-width="3" 
class="flow"/>
+
+  <rect x="410" y="95" width="390" height="260" rx="16" fill="#0f172a"/>
+  <text x="430" y="128" fill="#94a3b8" font-size="12" 
font-weight="700">response.requestTracing</text>
+  <text x="430" y="160" fill="#34d399" font-size="12">event-collection</text>
+  <text x="450" y="186" fill="#a78bfa" 
font-size="11">condition-evaluation</text>
+  <text x="470" y="210" fill="#94a3b8" font-size="11">property | boolean</text>
+  <text x="450" y="240" fill="#a78bfa" font-size="11">action-execution</text>
+  <text x="470" y="264" fill="#94a3b8" font-size="11">increment-property</text>
+  <text x="470" y="288" fill="#94a3b8" font-size="11">evaluate-segments</text>
+  <text x="430" y="322" fill="#64748b" font-size="11">startTime / endTime / 
traces[]</text>
+
+</svg>
diff --git a/src/main/webapp/assets/images/unomi3/shell-commands.svg 
b/src/main/webapp/assets/images/unomi3/shell-commands.svg
new file mode 100644
index 0000000..54f6e9f
--- /dev/null
+++ b/src/main/webapp/assets/images/unomi3/shell-commands.svg
@@ -0,0 +1,80 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg viewBox="0 0 840 400" fill="none" xmlns="http://www.w3.org/2000/svg"; 
role="img"
+     aria-label="Unomi 3 Karaf shell: unomi:crud, tenant context, cache, and 
task commands">
+  <defs><style>
+  svg {
+    --unomi-primary: #6d5ce7;
+    --unomi-primary-dark: #5a48c9;
+    --unomi-primary-light: #ede9fe;
+    --unomi-accent: #00cec9;
+    --unomi-text: #1e293b;
+    --unomi-text-muted: #64748b;
+    --unomi-white: #ffffff;
+    --unomi-bg-soft: #f8fafc;
+    --unomi-border: #e2e8f0;
+    --unomi-success: #059669;
+    --unomi-success-bg: #ecfdf5;
+    --unomi-amber: #d97706;
+    --unomi-amber-bg: #fef3c7;
+    --unomi-info: #2563eb;
+    --unomi-info-bg: #dbeafe;
+    font-family: Inter, system-ui, sans-serif;
+  }
+  @keyframes dash-flow { to { stroke-dashoffset: -24; } }
+  @keyframes float-y { 0%,100%{transform:translateY(0)} 
50%{transform:translateY(-3px)} }
+  @keyframes glow { 0%,100%{filter:drop-shadow(0 0 4px rgba(109,92,231,.35))} 
50%{filter:drop-shadow(0 0 10px rgba(109,92,231,.55))} }
+  .flow { stroke-dasharray: 6 5; animation: dash-flow 1.6s linear infinite; }
+  .float { animation: float-y 3.2s ease-in-out infinite; }
+  .glow { animation: glow 3s ease-in-out infinite; }
+  @media (prefers-reduced-motion: reduce) {
+    .flow,.float,.glow { animation: none !important; }
+  }
+</style></defs>
+  <rect width="840" height="400" rx="20" fill="var(--unomi-bg-soft)"/>
+  <text x="40" y="40" fill="var(--unomi-text)" font-size="18" 
font-weight="700">Karaf shell: unomi:*</text>
+  <text x="40" y="62" fill="var(--unomi-text-muted)" font-size="13">Unified 
CRUD, tenant session, cache ops, and task scheduler commands</text>
+
+  <rect x="40" y="95" width="250" height="70" rx="14" 
fill="var(--unomi-primary)" class="glow"/>
+  <text x="165" y="125" text-anchor="middle" fill="#fff" font-size="15" 
font-weight="700">ssh -p 8102</text>
+  <text x="165" y="148" text-anchor="middle" fill="rgba(255,255,255,.85)" 
font-size="11">karaf@localhost</text>
+
+  <path d="M300 130 H360" stroke="var(--unomi-primary)" stroke-width="3" 
class="flow"/>
+
+  <g class="float">
+    <rect x="370" y="90" width="200" height="100" rx="14" 
fill="var(--unomi-info-bg)" stroke="var(--unomi-info)" stroke-width="2"/>
+    <text x="470" y="125" text-anchor="middle" fill="var(--unomi-info)" 
font-size="13" font-weight="700">unomi:crud</text>
+    <text x="470" y="150" text-anchor="middle" fill="var(--unomi-text)" 
font-size="11">create read update</text>
+    <text x="470" y="172" text-anchor="middle" fill="var(--unomi-text)" 
font-size="11">delete list help</text>
+  </g>
+
+  <g class="float" style="animation-delay:.25s">
+    <rect x="590" y="90" width="210" height="100" rx="14" 
fill="var(--unomi-success-bg)" stroke="var(--unomi-success)" stroke-width="2"/>
+    <text x="695" y="125" text-anchor="middle" fill="var(--unomi-success)" 
font-size="13" font-weight="700">tenant-get / set</text>
+    <text x="695" y="150" text-anchor="middle" fill="var(--unomi-text)" 
font-size="11">session tenant context</text>
+    <text x="695" y="172" text-anchor="middle" fill="var(--unomi-text)" 
font-size="11">for CRUD + cache</text>
+  </g>
+
+  <g class="float" style="animation-delay:.5s">
+    <rect x="40" y="230" width="250" height="130" rx="14" 
fill="var(--unomi-amber-bg)" stroke="var(--unomi-amber)" stroke-width="2"/>
+    <text x="165" y="270" text-anchor="middle" fill="var(--unomi-amber)" 
font-size="13" font-weight="700">unomi:cache</text>
+    <text x="165" y="298" text-anchor="middle" fill="var(--unomi-text)" 
font-size="11">stats | clear | inspect</text>
+    <text x="165" y="322" text-anchor="middle" fill="var(--unomi-text)" 
font-size="11">view | remove | watch</text>
+    <text x="165" y="346" text-anchor="middle" fill="var(--unomi-text-muted)" 
font-size="11">per tenant / type</text>
+  </g>
+
+  <g class="float" style="animation-delay:.75s">
+    <rect x="310" y="230" width="250" height="130" rx="14" 
fill="var(--unomi-primary-light)" stroke="var(--unomi-primary)" 
stroke-width="2"/>
+    <text x="435" y="270" text-anchor="middle" 
fill="var(--unomi-primary-dark)" font-size="13" 
font-weight="700">unomi:task-*</text>
+    <text x="435" y="298" text-anchor="middle" fill="var(--unomi-text)" 
font-size="11">list show cancel</text>
+    <text x="435" y="322" text-anchor="middle" fill="var(--unomi-text)" 
font-size="11">retry purge executor</text>
+    <text x="435" y="346" text-anchor="middle" fill="var(--unomi-text-muted)" 
font-size="11">UNOMI-879 / 880</text>
+  </g>
+
+  <g class="float" style="animation-delay:1s">
+    <rect x="580" y="230" width="220" height="130" rx="14" fill="#0f172a"/>
+    <text x="690" y="270" text-anchor="middle" fill="#94a3b8" font-size="12" 
font-weight="700">Also</text>
+    <text x="690" y="298" text-anchor="middle" fill="#34d399" 
font-size="11">list-invalid-objects</text>
+    <text x="690" y="322" text-anchor="middle" fill="#a78bfa" 
font-size="11">rule-tail | event-tail</text>
+    <text x="690" y="346" text-anchor="middle" fill="#94a3b8" 
font-size="11">lifecycle | deploy</text>
+  </g>
+</svg>
diff --git a/src/main/webapp/assets/images/unomi3/task-scheduler.svg 
b/src/main/webapp/assets/images/unomi3/task-scheduler.svg
new file mode 100644
index 0000000..4d0c4b5
--- /dev/null
+++ b/src/main/webapp/assets/images/unomi3/task-scheduler.svg
@@ -0,0 +1,71 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg viewBox="0 0 840 400" fill="none" xmlns="http://www.w3.org/2000/svg"; 
role="img"
+     aria-label="Unomi 3.1 cluster-aware task scheduler with REST and shell">
+  <defs>
+<style>
+  svg {
+    --unomi-primary: #6d5ce7;
+    --unomi-primary-dark: #5a48c9;
+    --unomi-primary-light: #ede9fe;
+    --unomi-primary-subtle: #f5f3ff;
+    --unomi-accent: #00cec9;
+    --unomi-hero-accent: #a78bfa;
+    --unomi-text: #1e293b;
+    --unomi-text-muted: #64748b;
+    --unomi-white: #ffffff;
+    --unomi-bg-soft: #f8fafc;
+    --unomi-border: #e2e8f0;
+    --unomi-success: #059669;
+    --unomi-success-bg: #ecfdf5;
+    --unomi-amber: #d97706;
+    --unomi-amber-bg: #fef3c7;
+    --unomi-info: #2563eb;
+    --unomi-info-bg: #dbeafe;
+    font-family: Inter, system-ui, sans-serif;
+  }
+  @keyframes dash-flow { to { stroke-dashoffset: -24; } }
+  @keyframes pulse-soft { 0%,100%{opacity:.55} 50%{opacity:.9} }
+  @keyframes float-y { 0%,100%{transform:translateY(0)} 
50%{transform:translateY(-3px)} }
+  @keyframes glow { 0%,100%{filter:drop-shadow(0 0 4px rgba(109,92,231,.35))} 
50%{filter:drop-shadow(0 0 10px rgba(109,92,231,.55))} }
+  .flow { stroke-dasharray: 6 5; animation: dash-flow 1.6s linear infinite; }
+  .pulse { animation: pulse-soft 3s ease-in-out infinite; }
+  .float { animation: float-y 3.2s ease-in-out infinite; }
+  .glow { animation: glow 3s ease-in-out infinite; }
+  @media (prefers-reduced-motion: reduce) {
+    .flow,.pulse,.float,.glow { animation: none !important; }
+  }
+</style>
+  </defs>
+  <rect width="840" height="400" rx="20" fill="var(--unomi-bg-soft)"/>
+  <text x="40" y="40" fill="var(--unomi-text)" font-size="18" 
font-weight="700">Cluster-aware task scheduler</text>
+  <text x="40" y="62" fill="var(--unomi-text-muted)" font-size="13">Durable 
tasks, locks, retry and resume across Unomi nodes</text>
+
+  <rect x="300" y="95" width="240" height="70" rx="14" 
fill="var(--unomi-primary)" class="glow"/>
+  <text x="420" y="125" text-anchor="middle" fill="#fff" font-size="15" 
font-weight="700">SchedulerService</text>
+  <text x="420" y="148" text-anchor="middle" fill="rgba(255,255,255,.85)" 
font-size="11">persisted tasks + locks</text>
+
+  <g class="float">
+    <rect x="40" y="200" width="230" height="150" rx="14" 
fill="var(--unomi-info-bg)" stroke="var(--unomi-info)" stroke-width="2"/>
+    <text x="155" y="235" text-anchor="middle" fill="var(--unomi-info)" 
font-size="13" font-weight="700">REST /cxs/tasks</text>
+    <text x="155" y="262" text-anchor="middle" fill="var(--unomi-text)" 
font-size="11">list | show | cancel</text>
+    <text x="155" y="286" text-anchor="middle" fill="var(--unomi-text)" 
font-size="11">retry | resume</text>
+    <text x="155" y="318" text-anchor="middle" fill="var(--unomi-text-muted)" 
font-size="11">admin / tenant-admin</text>
+  </g>
+  <g class="float" style="animation-delay:.3s">
+    <rect x="305" y="200" width="230" height="150" rx="14" 
fill="var(--unomi-amber-bg)" stroke="var(--unomi-amber)" stroke-width="2"/>
+    <text x="420" y="235" text-anchor="middle" fill="var(--unomi-amber)" 
font-size="13" font-weight="700">Built-in types</text>
+    <text x="420" y="262" text-anchor="middle" fill="var(--unomi-text)" 
font-size="11">profile-purge</text>
+    <text x="420" y="286" text-anchor="middle" fill="var(--unomi-text)" 
font-size="11">segment-date-recalculation</text>
+    <text x="420" y="310" text-anchor="middle" fill="var(--unomi-text)" 
font-size="11">rules-statistics-refresh</text>
+    <text x="420" y="334" text-anchor="middle" fill="var(--unomi-text-muted)" 
font-size="11">cache-refresh-* | task-purge</text>
+  </g>
+  <g class="float" style="animation-delay:.6s">
+    <rect x="570" y="200" width="230" height="150" rx="14" 
fill="var(--unomi-success-bg)" stroke="var(--unomi-success)" stroke-width="2"/>
+    <text x="685" y="235" text-anchor="middle" fill="var(--unomi-success)" 
font-size="13" font-weight="700">Karaf shell</text>
+    <text x="685" y="262" text-anchor="middle" fill="var(--unomi-text)" 
font-size="11">unomi:task-list</text>
+    <text x="685" y="286" text-anchor="middle" fill="var(--unomi-text)" 
font-size="11">task-show | task-retry</text>
+    <text x="685" y="310" text-anchor="middle" fill="var(--unomi-text)" 
font-size="11">task-cancel | task-purge</text>
+    <text x="685" y="334" text-anchor="middle" fill="var(--unomi-text-muted)" 
font-size="11">task-executor</text>
+  </g>
+
+</svg>
diff --git a/src/main/webapp/assets/images/unomi3/unit-harness-vs-it.svg 
b/src/main/webapp/assets/images/unomi3/unit-harness-vs-it.svg
index 043fdcd..6c4b19f 100644
--- a/src/main/webapp/assets/images/unomi3/unit-harness-vs-it.svg
+++ b/src/main/webapp/assets/images/unomi3/unit-harness-vs-it.svg
@@ -45,26 +45,26 @@
   <!-- Unit track -->
   <g>
     <rect x="40" y="90" width="360" height="210" rx="18" 
fill="var(--unomi-success-bg)" stroke="var(--unomi-success)" stroke-width="2"/>
-    <text x="60" y="122" fill="var(--unomi-success)" font-size="14" 
font-weight="700">UNIT · seconds</text>
+    <text x="60" y="122" fill="var(--unomi-success)" font-size="14" 
font-weight="700">UNIT | seconds</text>
     <rect x="60" y="140" width="320" height="44" rx="12" 
fill="var(--unomi-white)" class="glow"/>
     <text x="220" y="168" text-anchor="middle" fill="var(--unomi-text)" 
font-size="13" font-weight="600">InMemoryPersistenceServiceImpl</text>
-    <text x="80" y="210" fill="var(--unomi-text)" font-size="12">→ TestHelper 
wires real *ServiceImpl</text>
-    <text x="80" y="232" fill="var(--unomi-text)" font-size="12">→ conditions 
· refresh simulation</text>
-    <text x="80" y="254" fill="var(--unomi-text)" font-size="12">→ no Docker · 
no Karaf boot</text>
-    <text x="80" y="280" fill="var(--unomi-success)" font-size="12" 
font-weight="600">Independent · high-signal</text>
+    <text x="80" y="210" fill="var(--unomi-text)" font-size="12">-> TestHelper 
wires real *ServiceImpl</text>
+    <text x="80" y="232" fill="var(--unomi-text)" font-size="12">-> conditions 
| refresh simulation</text>
+    <text x="80" y="254" fill="var(--unomi-text)" font-size="12">-> no Docker 
| no Karaf boot</text>
+    <text x="80" y="280" fill="var(--unomi-success)" font-size="12" 
font-weight="600">Independent | high-signal</text>
   </g>
 
   <!-- IT track -->
   <g>
     <rect x="440" y="90" width="360" height="210" rx="18" 
fill="var(--unomi-info-bg)" stroke="var(--unomi-info)" stroke-width="2"/>
-    <text x="460" y="122" fill="var(--unomi-info)" font-size="14" 
font-weight="700">INTEGRATION · minutes+</text>
+    <text x="460" y="122" fill="var(--unomi-info)" font-size="14" 
font-weight="700">INTEGRATION | minutes+</text>
     <rect x="460" y="140" width="140" height="44" rx="12" 
fill="var(--unomi-white)" class="float"/>
     <text x="530" y="168" text-anchor="middle" fill="var(--unomi-text)" 
font-size="12" font-weight="600">Karaf</text>
     <rect x="620" y="140" width="160" height="44" rx="12" 
fill="var(--unomi-white)" class="float" style="animation-delay:.4s"/>
     <text x="700" y="168" text-anchor="middle" fill="var(--unomi-text)" 
font-size="12" font-weight="600">ES / OpenSearch</text>
-    <text x="480" y="210" fill="var(--unomi-text)" font-size="12">→ Pax Exam + 
Docker stack</text>
-    <text x="480" y="232" fill="var(--unomi-text)" font-size="12">→ full REST 
· clustering · tenants</text>
-    <text x="480" y="254" fill="var(--unomi-text)" font-size="12">→ proves the 
live platform</text>
+    <text x="480" y="210" fill="var(--unomi-text)" font-size="12">-> Pax Exam 
+ Docker stack</text>
+    <text x="480" y="232" fill="var(--unomi-text)" font-size="12">-> full REST 
| clustering | tenants</text>
+    <text x="480" y="254" fill="var(--unomi-text)" font-size="12">-> proves 
the live platform</text>
     <text x="480" y="280" fill="var(--unomi-info)" font-size="12" 
font-weight="600">End-to-end confidence</text>
   </g>
 </svg>
diff --git a/src/main/webapp/blog/index.html b/src/main/webapp/blog/index.html
index 174b9f7..c83f6e5 100644
--- a/src/main/webapp/blog/index.html
+++ b/src/main/webapp/blog/index.html
@@ -111,6 +111,210 @@ layout: default
     </div>
 </section>
 
+<!-- DX: build.sh -->
+<section class="section section-alt">
+    <div class="container">
+        <div class="row align-items-center g-4 g-lg-5">
+            <div class="col-lg-6">
+                <span class="badge badge-primary mb-3">Developer 
experience</span>
+                <h2 class="h3 fw-bold mb-3">
+                    <a href="/blog/unomi-3-build-sh.html" 
class="text-decoration-none text-body">One entry point: build.sh</a>
+                </h2>
+                <p class="text-muted mb-4">Preflight, Maven build, integration 
tests, and deploy &mdash; one script locally and on GitHub Actions, with a 
<code>build.ps1</code> twin for Windows.</p>
+                <a href="/blog/unomi-3-build-sh.html" class="btn btn-primary">
+                    Read the build.sh post <i class="bi bi-arrow-right 
ms-1"></i>
+                </a>
+            </div>
+            <div class="col-lg-6">
+                <a href="/blog/unomi-3-build-sh.html" class="d-block rounded-3 
overflow-hidden shadow-sm" aria-label="Read build.sh post">
+                    <img src="/assets/images/unomi3/build-sh.svg" 
class="img-fluid w-100" alt="build.sh hub for preflight build test deploy" 
loading="lazy">
+                </a>
+            </div>
+        </div>
+    </div>
+</section>
+
+<!-- Docs: Manual -->
+<section class="section">
+    <div class="container">
+        <div class="row align-items-center g-4 g-lg-5">
+            <div class="col-lg-6 order-lg-2">
+                <span class="badge badge-primary mb-3">Documentation</span>
+                <h2 class="h3 fw-bold mb-3">
+                    <a href="/blog/unomi-3-manual.html" 
class="text-decoration-none text-body">Unomi 3 manual improvements</a>
+                </h2>
+                <p class="text-muted mb-4">PlantUML diagrams in AsciiDoc, an 
Unomi-branded HTML/PDF theme, and deep chapters for migrations, multi-tenancy, 
upgrades, and conditions.</p>
+                <a href="/blog/unomi-3-manual.html" class="btn btn-primary">
+                    Read the manual post <i class="bi bi-arrow-right ms-1"></i>
+                </a>
+            </div>
+            <div class="col-lg-6 order-lg-1">
+                <a href="/blog/unomi-3-manual.html" class="d-block rounded-3 
overflow-hidden shadow-sm" aria-label="Read Unomi 3 manual improvements">
+                    <img src="/assets/images/unomi3/manual-plantuml-theme.svg" 
class="img-fluid w-100" alt="PlantUML source to branded Unomi manual" 
loading="lazy">
+                </a>
+            </div>
+        </div>
+    </div>
+</section>
+
+<!-- Quality: IT execution -->
+<section class="section section-alt">
+    <div class="container">
+        <div class="row align-items-center g-4 g-lg-5">
+            <div class="col-lg-6">
+                <span class="badge badge-primary mb-3">Quality &middot; 
Developer experience</span>
+                <h2 class="h3 fw-bold mb-3">
+                    <a href="/blog/unomi-3-it-execution.html" 
class="text-decoration-none text-body">Clearer integration test runs</a>
+                </h2>
+                <p class="text-muted mb-4">Live progress bars with ETA, 
quieter Maven/Docker CI output, and tooling to archive and compare long IT 
suites &mdash; before / after from real GitHub Actions runs.</p>
+                <a href="/blog/unomi-3-it-execution.html" class="btn 
btn-primary">
+                    Read the IT execution post <i class="bi bi-arrow-right 
ms-1"></i>
+                </a>
+            </div>
+            <div class="col-lg-6">
+                <a href="/blog/unomi-3-it-execution.html" class="d-block 
rounded-3 overflow-hidden shadow-sm" aria-label="Read clearer integration test 
runs">
+                    <img 
src="/assets/images/unomi3/it-progress-before-after.svg" class="img-fluid 
w-100" alt="IT console before and after progress bars" loading="lazy">
+                </a>
+            </div>
+        </div>
+    </div>
+</section>
+
+<!-- Quality: unit harness -->
+<section class="section">
+    <div class="container">
+        <div class="row align-items-center g-4 g-lg-5">
+            <div class="col-lg-6 order-lg-2">
+                <span class="badge badge-primary mb-3">Quality &middot; Unit 
tests</span>
+                <h2 class="h3 fw-bold mb-3">
+                    <a href="/blog/unomi-3-unit-testing-harness.html" 
class="text-decoration-none text-body">Faster service unit tests</a>
+                </h2>
+                <p class="text-muted 
mb-4"><code>InMemoryPersistenceServiceImpl</code> &mdash; a full 
PersistenceService on the test classpath so Profile, Event, Rules, and Segment 
logic runs in seconds without Karaf.</p>
+                <a href="/blog/unomi-3-unit-testing-harness.html" class="btn 
btn-primary">
+                    Read the unit harness post <i class="bi bi-arrow-right 
ms-1"></i>
+                </a>
+            </div>
+            <div class="col-lg-6 order-lg-1">
+                <a href="/blog/unomi-3-unit-testing-harness.html" 
class="d-block rounded-3 overflow-hidden shadow-sm" aria-label="Read unit 
testing harness">
+                    <img src="/assets/images/unomi3/unit-harness-vs-it.svg" 
class="img-fluid w-100" alt="Unit harness versus integration tests" 
loading="lazy">
+                </a>
+            </div>
+        </div>
+    </div>
+</section>
+
+<!-- Platform + OpenSearch pair -->
+<section class="section section-alt">
+    <div class="container">
+        <div class="row justify-content-center text-center mb-4">
+            <div class="col-lg-7">
+                <p class="section-label">Platform &amp; ops</p>
+                <h2 class="h4 mb-0">Foundation, search, shell, scheduler, 
tracing</h2>
+            </div>
+        </div>
+        <div class="row g-4">
+            <div class="col-md-6 col-lg-3">
+                <article class="feature-card h-100 overflow-hidden p-0 
position-relative">
+                    <a href="/blog/unomi-3-platform-foundation.html" 
class="d-block" aria-label="Read platform foundation">
+                        <img 
src="/assets/images/unomi3/platform-foundation.svg" class="img-fluid w-100" 
alt="Unomi 3 platform foundation" loading="lazy">
+                    </a>
+                    <div class="p-3">
+                        <span class="badge badge-primary mb-2">Platform</span>
+                        <h3 class="h6 fw-semibold mb-2">
+                            <a href="/blog/unomi-3-platform-foundation.html" 
class="text-decoration-none text-body">Platform foundation</a>
+                        </h3>
+                        <p class="small text-muted mb-0">Java 17, Karaf 4.4, 
ES9, persistence clustering.</p>
+                        <a href="/blog/unomi-3-platform-foundation.html" 
class="stretched-link" aria-label="Read platform foundation"></a>
+                    </div>
+                </article>
+            </div>
+            <div class="col-md-6 col-lg-3">
+                <article class="feature-card h-100 overflow-hidden p-0 
position-relative">
+                    <a href="/blog/unomi-3-opensearch.html" class="d-block" 
aria-label="Read OpenSearch post">
+                        <img src="/assets/images/unomi3/opensearch-dual.svg" 
class="img-fluid w-100" alt="Elasticsearch and OpenSearch" loading="lazy">
+                    </a>
+                    <div class="p-3">
+                        <span class="badge badge-primary mb-2">Ops</span>
+                        <h3 class="h6 fw-semibold mb-2">
+                            <a href="/blog/unomi-3-opensearch.html" 
class="text-decoration-none text-body">ES9 &amp; OpenSearch</a>
+                        </h3>
+                        <p class="small text-muted mb-0">Dual backends, same 
SPI, migrate guide.</p>
+                        <a href="/blog/unomi-3-opensearch.html" 
class="stretched-link" aria-label="Read OpenSearch post"></a>
+                    </div>
+                </article>
+            </div>
+            <div class="col-md-6 col-lg-3">
+                <article class="feature-card h-100 overflow-hidden p-0 
position-relative">
+                    <a href="/blog/unomi-3-shell-commands.html" 
class="d-block" aria-label="Read shell commands">
+                        <img src="/assets/images/unomi3/shell-commands.svg" 
class="img-fluid w-100" alt="Unomi Karaf shell commands" loading="lazy">
+                    </a>
+                    <div class="p-3">
+                        <span class="badge badge-primary mb-2">Ops</span>
+                        <h3 class="h6 fw-semibold mb-2">
+                            <a href="/blog/unomi-3-shell-commands.html" 
class="text-decoration-none text-body">Shell commands</a>
+                        </h3>
+                        <p class="small text-muted 
mb-0"><code>unomi:crud</code>, tenant, cache, tasks.</p>
+                        <a href="/blog/unomi-3-shell-commands.html" 
class="stretched-link" aria-label="Read shell commands"></a>
+                    </div>
+                </article>
+            </div>
+            <div class="col-md-6 col-lg-3">
+                <article class="feature-card h-100 overflow-hidden p-0 
position-relative">
+                    <a href="/blog/unomi-3-task-scheduler.html" 
class="d-block" aria-label="Read task scheduler">
+                        <img src="/assets/images/unomi3/task-scheduler.svg" 
class="img-fluid w-100" alt="Task scheduler" loading="lazy">
+                    </a>
+                    <div class="p-3">
+                        <span class="badge badge-primary mb-2">3.1</span>
+                        <h3 class="h6 fw-semibold mb-2">
+                            <a href="/blog/unomi-3-task-scheduler.html" 
class="text-decoration-none text-body">Task scheduler</a>
+                        </h3>
+                        <p class="small text-muted mb-0">REST 
<code>/cxs/tasks</code>, shell, cluster locks.</p>
+                        <a href="/blog/unomi-3-task-scheduler.html" 
class="stretched-link" aria-label="Read task scheduler"></a>
+                    </div>
+                </article>
+            </div>
+            <div class="col-md-6 col-lg-3">
+                <article class="feature-card h-100 overflow-hidden p-0 
position-relative">
+                    <a href="/blog/unomi-3-request-tracing.html" 
class="d-block" aria-label="Read request tracing">
+                        <img src="/assets/images/unomi3/request-tracing.svg" 
class="img-fluid w-100" alt="Request tracing explain" loading="lazy">
+                    </a>
+                    <div class="p-3">
+                        <span class="badge badge-primary mb-2">Debug</span>
+                        <h3 class="h6 fw-semibold mb-2">
+                            <a href="/blog/unomi-3-request-tracing.html" 
class="text-decoration-none text-body">Request tracing</a>
+                        </h3>
+                        <p class="small text-muted 
mb-0"><code>explain=true</code> with verified curl examples.</p>
+                        <a href="/blog/unomi-3-request-tracing.html" 
class="stretched-link" aria-label="Read request tracing"></a>
+                    </div>
+                </article>
+            </div>
+        </div>
+    </div>
+</section>
+
+<!-- OpenAPI enrichment -->
+<section class="section">
+    <div class="container">
+        <div class="row align-items-center g-4 g-lg-5">
+            <div class="col-lg-6">
+                <span class="badge badge-primary mb-3">Unomi 3.1 &middot; 
REST</span>
+                <h2 class="h3 fw-bold mb-3">
+                    <a href="/blog/unomi-3-openapi-enrichment.html" 
class="text-decoration-none text-body">Richer OpenAPI from Javadoc</a>
+                </h2>
+                <p class="text-muted mb-4">Unomi 3.1 merges endpoint Javadoc 
into <code>/cxs/openapi.json</code> &mdash; summaries, statuses, examples, and 
schema notes for humans and agents (UNOMI-963).</p>
+                <a href="/blog/unomi-3-openapi-enrichment.html" class="btn 
btn-primary">
+                    Read the OpenAPI post <i class="bi bi-arrow-right 
ms-1"></i>
+                </a>
+            </div>
+            <div class="col-lg-6">
+                <a href="/blog/unomi-3-openapi-enrichment.html" class="d-block 
rounded-3 overflow-hidden shadow-sm" aria-label="Read OpenAPI enrichment">
+                    <img src="/assets/images/unomi3/openapi-enrichment.svg" 
class="img-fluid w-100" alt="OpenAPI enrichment pipeline" loading="lazy">
+                </a>
+            </div>
+        </div>
+    </div>
+</section>
+
 <!-- Welcome + topics -->
 <section class="section">
     <div class="container">
@@ -140,6 +344,16 @@ layout: default
                         <a href="/blog/unomi-3-multitenancy.html" class="badge 
badge-primary text-decoration-none px-3 py-2">Multi-tenancy</a>
                         <a href="/blog/unomi-3-ai-agents.html" class="badge 
badge-primary text-decoration-none px-3 py-2">AI agents</a>
                         <a href="/blog/migrating-to-unomi-3.html" class="badge 
badge-primary text-decoration-none px-3 py-2">Migration</a>
+                        <a href="/blog/unomi-3-it-execution.html" class="badge 
badge-primary text-decoration-none px-3 py-2">IT execution</a>
+                        <a href="/blog/unomi-3-manual.html" class="badge 
badge-primary text-decoration-none px-3 py-2">Manual</a>
+                        <a href="/blog/unomi-3-build-sh.html" class="badge 
badge-primary text-decoration-none px-3 py-2">build.sh</a>
+                        <a href="/blog/unomi-3-unit-testing-harness.html" 
class="badge badge-primary text-decoration-none px-3 py-2">Unit harness</a>
+                        <a href="/blog/unomi-3-platform-foundation.html" 
class="badge badge-primary text-decoration-none px-3 py-2">Platform</a>
+                        <a href="/blog/unomi-3-opensearch.html" class="badge 
badge-primary text-decoration-none px-3 py-2">OpenSearch</a>
+                        <a href="/blog/unomi-3-shell-commands.html" 
class="badge badge-primary text-decoration-none px-3 py-2">Shell</a>
+                        <a href="/blog/unomi-3-task-scheduler.html" 
class="badge badge-primary text-decoration-none px-3 py-2">Scheduler</a>
+                        <a href="/blog/unomi-3-request-tracing.html" 
class="badge badge-primary text-decoration-none px-3 py-2">Tracing</a>
+                        <a href="/blog/unomi-3-openapi-enrichment.html" 
class="badge badge-primary text-decoration-none px-3 py-2">OpenAPI</a>
                         <a href="/use-cases.html" class="badge badge-secondary 
text-decoration-none px-3 py-2">Use cases</a>
                         <a href="/community/events/index.html" class="badge 
badge-secondary text-decoration-none px-3 py-2">Events</a>
                     </div>
@@ -151,7 +365,7 @@ layout: default
 </section>
 
 <!-- Reading order -->
-<section class="section section-alt">
+<section class="section">
     <div class="container">
         <div class="row justify-content-center text-center mb-4">
             <div class="col-lg-7">
@@ -191,6 +405,15 @@ layout: default
         </div>
         <p class="text-center small text-muted mt-4 mb-0">
             Then: <a href="/blog/migrating-to-unomi-3.html">Migrating from 
2.x</a> &middot;
+            <a href="/blog/unomi-3-platform-foundation.html">Platform</a> 
&middot;
+            <a href="/blog/unomi-3-opensearch.html">OpenSearch</a> &middot;
+            <a href="/blog/unomi-3-shell-commands.html">Shell</a> &middot;
+            <a href="/blog/unomi-3-unit-testing-harness.html">Unit harness</a> 
&middot;
+            <a href="/blog/unomi-3-request-tracing.html">Tracing</a> &middot;
+            <a href="/blog/unomi-3-openapi-enrichment.html">OpenAPI</a> 
&middot;
+            <a href="/blog/unomi-3-build-sh.html">build.sh</a> &middot;
+            <a href="/blog/unomi-3-manual.html">Manual</a> &middot;
+            <a href="/blog/unomi-3-it-execution.html">IT execution</a> &middot;
             <a href="/download.html">Download</a> &middot;
             <a href="/get-started.html">Get started</a>
         </p>
diff --git a/src/main/webapp/blog/unomi-3-build-sh.html 
b/src/main/webapp/blog/unomi-3-build-sh.html
new file mode 100644
index 0000000..e06759e
--- /dev/null
+++ b/src/main/webapp/blog/unomi-3-build-sh.html
@@ -0,0 +1,79 @@
+---
+title: One entry point — Unomi 3 build.sh
+description: "How Apache Unomi 3 replaced scattered build scripts with a 
single build.sh — preflight checks, CI mode, integration tests, deploy, and 
Windows build.ps1 parity."
+keywords: "Unomi build.sh, Apache Unomi developer experience, UNOMI-887, Maven 
CI, OpenSearch integration tests"
+layout: default
+---
+
+<section class="page-header">
+    <div class="container">
+        <p class="section-label text-hero-label">Unomi 3 &middot; Developer 
experience</p>
+        <h1>One entry point: <code>build.sh</code></h1>
+        <p class="lead">Preflight, build, test, and deploy &mdash; the same 
script locally and on GitHub Actions.</p>
+    </div>
+</section>
+
+<section class="section">
+    <div class="container col-lg-8">
+        <p>Before Unomi 3, contributors juggled several ad-hoc scripts 
(<code>buildAndRun.sh</code>, <code>compileDeploy.sh</code>, and friends).
+            <a 
href="https://issues.apache.org/jira/browse/UNOMI-887";>UNOMI-887</a>
+            /
+            <a href="https://github.com/apache/unomi/pull/737";>#737</a>
+            folded those workflows into a single
+            <strong><code>./build.sh</code></strong>
+            at the repository root &mdash; with clearer errors, colorized 
output, and flags that match how people actually work.</p>
+
+        <img src="/assets/images/unomi3/build-sh.svg" class="img-fluid my-4" 
alt="build.sh as one hub for preflight, build, test, and deploy"/>
+
+        <h2 class="h4 mt-4 mb-3">What it replaced</h2>
+        <p>Legacy one-off scripts for &ldquo;build and run,&rdquo; 
&ldquo;build without tests,&rdquo; packaging, and deploy were removed in favor 
of one maintained entry point.
+            The PowerShell twin
+            <code>build.ps1</code>
+            tracks the same flags for Windows contributors.</p>
+
+        <h2 class="h4 mt-5 mb-3">What you get</h2>
+        <ul>
+            <li><strong>Preflight</strong> &mdash; Java, Maven, GraphViz; 
memory/disk heuristics; ports for debug sessions; fail early with readable 
messages</li>
+            <li><strong>Build flexibility</strong> &mdash; skip tests, 
OpenSearch vs Elasticsearch, named distributions, offline / Maven debug, 
optional Javadoc gate</li>
+            <li><strong>Integration tests</strong> &mdash; 
<code>--integration-tests</code>, <code>--single-test</code>, IT debug/suspend, 
keep container, stream search logs when needed, memory sampler</li>
+            <li><strong>Run &amp; deploy</strong> &mdash; <code>--deploy 
--karaf-home</code>, Karaf debug ports, <code>--auto-start</code> for search 
engines, <code>--no-karaf</code> for package-only builds</li>
+            <li><strong>CI mode</strong> &mdash; <code>--ci</code> is 
non-interactive, batch Maven (<code>-B -ntp</code>), no Karaf side-trips; 
GitHub Actions calls the same script as you do locally (<a 
href="https://github.com/apache/unomi/pull/764";>#764</a>)</li>
+            <li><strong>UX details</strong> &mdash; section headers, traps 
with line/context on failure, <code>NO_COLOR</code> respected, optional 
<code>--log-file</code> tee</li>
+        </ul>
+
+        <h2 class="h4 mt-5 mb-3">Try it</h2>
+        <pre class="code-block-light p-3 rounded-3 small 
overflow-auto"><code>./build.sh --help
+
+# Default install
+./build.sh
+
+# Match GitHub Actions shape
+./build.sh --ci
+
+# Full IT matrix flavors
+./build.sh --integration-tests
+./build.sh --integration-tests --use-opensearch
+
+# One flaky test under a debugger
+./build.sh --integration-tests \
+  --single-test org.apache.unomi.itests.graphql.GraphQLEventIT \
+  --it-debug --it-debug-suspend
+
+# Deploy KAR into an existing Karaf
+./build.sh --deploy --karaf-home ~/apache-karaf</code></pre>
+
+        <h2 class="h4 mt-5 mb-3">Why it matters</h2>
+        <p>Contributor friction kills review velocity. One documented command 
that validates the environment, builds the right distribution, and runs the 
same gates as CI means less &ldquo;works on my machine&rdquo; archaeology 
&mdash; and pairs with the
+            <a href="/blog/unomi-3-it-execution.html">clearer IT console</a>
+            and
+            <a href="/blog/unomi-3-manual.html">manual</a>
+            investments in Unomi 3.</p>
+
+        <p class="mt-4">
+            <a class="btn btn-primary" 
href="https://github.com/apache/unomi/blob/master/build.sh";>View build.sh on 
GitHub</a>
+            <a class="btn btn-outline-primary ms-2" href="{{ 
site.data.unomi.manual.published_latest }}#_using_the_build_script">Manual: 
using the build script</a>
+            <a class="btn btn-outline-primary ms-2" 
href="/contribute/testing.html">Contribute &middot; Testing</a>
+        </p>
+        <p><a href="/blog/">&larr; All posts</a></p>
+    </div>
+</section>
diff --git a/src/main/webapp/blog/unomi-3-it-execution.html 
b/src/main/webapp/blog/unomi-3-it-execution.html
new file mode 100644
index 0000000..e620d92
--- /dev/null
+++ b/src/main/webapp/blog/unomi-3-it-execution.html
@@ -0,0 +1,96 @@
+---
+title: Clearer integration test runs in Unomi 3
+description: "How Unomi 3 improved IT execution — live progress bars with ETA, 
quieter Maven and Docker output, and tooling for diagnosing long CI suites."
+keywords: "Unomi 3 integration tests, ProgressListener, CI logs, Apache Unomi 
IT, build.sh"
+layout: default
+---
+
+<section class="page-header">
+    <div class="container">
+        <p class="section-label text-hero-label">Unomi 3 &middot; Quality</p>
+        <h1>Clearer integration test runs in Unomi 3</h1>
+        <p class="lead">Progress bars, quieter CI output, and tools that turn 
a 90&ndash;minute suite into something you can actually follow.</p>
+    </div>
+</section>
+
+<section class="section">
+    <div class="container col-lg-8">
+        <p>Apache Unomi&rsquo;s integration tests are deliberately heavy: Pax 
Exam boots a real Karaf, Docker starts Elasticsearch or OpenSearch, and 
Failsafe walks a large end-to-end suite. That work takes on the order of
+            <strong>one to two hours</strong> on
+            <a 
href="https://github.com/apache/unomi/actions/workflows/unomi-ci-build-tests.yml";>GitHub
 Actions</a>
+            (for example
+            <a 
href="https://github.com/apache/unomi/actions/runs/29200354118";>run 
#29200354118</a>
+            on <code>master</code>, ~1h&nbsp;52m wall time). For years the 
console mostly said &ldquo;still downloading&rdquo; or &ldquo;still 
running&rdquo; &mdash; with almost no sense of
+            <em>how far</em>
+            or
+            <em>what was left</em>.</p>
+        <p>Unomi 3 invested in the
+            <strong>execution experience</strong>
+            of that suite: live progress, quieter logs, and developer tooling 
around archives and memory. The goal is not fewer tests &mdash; it is being 
able to watch (and debug) the ones we already run.</p>
+
+        <img src="/assets/images/unomi3/it-progress-before-after.svg" 
class="img-fluid my-4" alt="Before and after IT console: Maven download spam 
versus progress bar with ETA"/>
+
+        <h2 class="h4 mt-4 mb-3">What changed</h2>
+        <ul>
+            <li><strong>Live progress</strong> &mdash; <a 
href="https://issues.apache.org/jira/browse/UNOMI-912";>UNOMI-912</a> / <a 
href="https://github.com/apache/unomi/pull/735";>#735</a> added 
<code>ProgressSuite</code> + <code>ProgressListener</code>: accurate total, 
percentage, ETA, pass/fail counters, and a CSV of the slowest tests at the 
end.</li>
+            <li><strong>Quieter Maven in CI</strong> &mdash; <a 
href="https://github.com/apache/unomi/pull/764";>#764</a> aligned Actions with 
<code>./build.sh --ci</code>; follow-up work forces <code>-B -ntp</code> so 
transfer &ldquo;Progress (1): &hellip; kB&rdquo; lines stop drowning the 
log.</li>
+            <li><strong>Docker search logs off the console</strong> &mdash; 
Elasticsearch/OpenSearch container logs go to files under 
<code>itests/target/</code> by default; opt in with 
<code>--search-engine-logs</code> when you need them live.</li>
+            <li><strong>Diagnose, don&rsquo;t re-run blindly</strong> &mdash; 
<a href="https://github.com/apache/unomi/pull/770";>#770</a> (archive / compare 
/ <code>kt.sh</code>) and <a 
href="https://github.com/apache/unomi/pull/782";>#782</a> (memory sampling + CI 
artifacts) make flaky vs systematic failures easier to classify.</li>
+        </ul>
+
+        <h2 class="h4 mt-5 mb-3">Before / after (CI console)</h2>
+        <p>Raw job log downloads from Actions require repository privileges, 
so the excerpts below are
+            <strong>representative reconstructions</strong>
+            of what contributors and committers see in the &ldquo;Execute 
integration tests&rdquo; job &mdash; anchored to real public runs and the 
UNOMI-912 acceptance output.</p>
+
+        <p class="small text-muted mb-2"><strong>Before</strong> &mdash; 
typical shape of an older IT job before progress-aware running and transfer 
quieting (see e.g.
+            <a 
href="https://github.com/apache/unomi/actions/runs/19324846828";>run 
#19324846828</a>,
+            2025-11-13, pre&ndash;UNOMI-912 merge). Walls of Maven transfer 
progress, little suite-level signal:</p>
+        <pre class="p-3 rounded-3 small overflow-auto" style="max-height: 
14rem;"><code>Downloading from central: 
https://repo.maven.apache.org/.../junit-4.13.2.jar
+Progress (1): 4.1/384 kB
+Progress (1): 48/384 kB
+Progress (1): 128/384 kB
+Downloaded from central: .../junit-4.13.2.jar (0.4 s)
+Downloading from central: 
https://repo.maven.apache.org/.../hamcrest-core-1.3.jar
+Progress (1): 7.2/45 kB
+...
+[INFO] --- docker-maven-plugin: start (elasticsearch)
+... thousands of similar lines ...
+# Suite still running - no % complete, no ETA, no live fail list</code></pre>
+
+        <p class="small text-muted mb-2 mt-4"><strong>After</strong> &mdash; 
same class of job on Unomi 3 tip (e.g.
+            <a 
href="https://github.com/apache/unomi/actions/runs/29200354118";>run 
#29200354118</a>,
+            2026-07-12). Transfer UI suppressed; <code>ProgressListener</code> 
owns the &ldquo;are we stuck?&rdquo; question. Format from
+            <a 
href="https://issues.apache.org/jira/browse/UNOMI-912";>UNOMI-912</a>:</p>
+        <pre class="p-3 rounded-3 small overflow-auto" style="max-height: 
14rem;"><code>[#######-----------------------] Progress: 23.33% (35/150 tests).
+  Estimated time remaining: 4m 12s. Successful: 35, Failed: 0
+
+# ... later ...
+
+Top 10 Slowest Tests:
+Rank,Test Name,Duration (ms)
+1,"GraphQLProfileIT.testUpdateProperty",18234
+2,"ProfileMergeIT.testMerge",14925
+...</code></pre>
+
+        <h2 class="h4 mt-5 mb-3">Try it locally</h2>
+        <pre class="code-block-light p-3 rounded-3 small"><code>./build.sh 
--integration-tests
+# CI-shaped (batch + no transfer progress):
+./build.sh --ci --integration-tests</code></pre>
+        <p>After a run, see <code>itests/README.md</code> for 
<code>archive-it-run.sh</code>, <code>compare-it-runs.sh</code>, 
<code>kt.sh</code>, and memory sampling. On Actions, download the 
<code>it-memory-metrics-*</code> artifact from a completed IT job.</p>
+
+        <h2 class="h4 mt-5 mb-3">How this fits the Unomi 3 quality story</h2>
+        <p>Progress and cleaner logs make the
+            <em>long</em>
+            path usable. Unomi 3 also added a
+            <em>short</em>
+            path &mdash; the in-memory persistence harness for fast service 
unit tests without Karaf or Docker. Different layers, same idea: confidence 
without guessing.</p>
+
+        <p class="mt-4">
+            <a class="btn btn-primary" 
href="https://github.com/apache/unomi/tree/master/itests";>Browse itests on 
GitHub</a>
+            <a class="btn btn-outline-primary ms-2" 
href="/blog/introducing-apache-unomi-3.html">Unomi 3 intro</a>
+            <a class="btn btn-outline-primary ms-2" 
href="/contribute/testing.html">Contribute &middot; Testing</a>
+        </p>
+        <p><a href="/blog/">&larr; All posts</a></p>
+    </div>
+</section>
diff --git a/src/main/webapp/blog/unomi-3-manual.html 
b/src/main/webapp/blog/unomi-3-manual.html
new file mode 100644
index 0000000..cebe055
--- /dev/null
+++ b/src/main/webapp/blog/unomi-3-manual.html
@@ -0,0 +1,102 @@
+---
+title: Unomi 3 manual — PlantUML, branded theme, deeper docs
+description: "How the Apache Unomi 3 manual gained PlantUML/GraphViz diagrams, 
an Unomi-branded HTML/PDF theme, and major content expansions for migrations, 
multi-tenancy, and platform upgrades."
+keywords: "Apache Unomi manual, PlantUML AsciiDoc, Unomi 3 documentation, 
AsciiDoctor diagrams, Unomi CSS theme"
+layout: default
+---
+
+<section class="page-header">
+    <div class="container">
+        <p class="section-label text-hero-label">Unomi 3 &middot; 
Documentation</p>
+        <h1>Unomi 3 manual: diagrams, theme, and depth</h1>
+        <p class="lead">PlantUML in AsciiDoc, a brand-aligned HTML/PDF look, 
and chapters that finally match the Unomi 3 platform.</p>
+    </div>
+</section>
+
+<section class="section">
+    <div class="container col-lg-8">
+        <p>Shipping Unomi 3 without a manual that looks and reads like Unomi 3 
would leave operators guessing. Over several
+            <a 
href="https://issues.apache.org/jira/browse/UNOMI-882";>UNOMI-882</a>
+            and
+            <a 
href="https://issues.apache.org/jira/browse/UNOMI-960";>UNOMI-960</a>
+            efforts, the project rebuilt how the official manual is
+            <em>authored</em>,
+            <em>rendered</em>, and
+            <em>navigated</em>
+            &mdash; from diagram toolchain to CSS theme to whole new 
chapters.</p>
+
+        <img src="/assets/images/unomi3/manual-plantuml-theme.svg" 
class="img-fluid my-4" alt="AsciiDoc PlantUML source builds into a branded 
Unomi 3 manual with diagrams"/>
+
+        <h2 class="h4 mt-4 mb-3">PlantUML (and GraphViz) in the build</h2>
+        <p><a href="https://github.com/apache/unomi/pull/753";>#753</a> wired
+            <strong>AsciiDoctor Diagram</strong>
+            into <code>manual/pom.xml</code>: 
<code>asciidoctorj-diagram</code> + PlantUML, with GraphViz available via
+            <code>GRAPHVIZ_DOT</code> / <code>graphvizdot</code>. CI installs 
GraphViz so HTML and PDF builds stay reproducible.</p>
+        <p>Diagrams live
+            <strong>in the AsciiDoc</strong>
+            as <code>[plantuml]</code> blocks &mdash; reviewable in pull 
requests, regenerated on every docs build, no orphaned PNGs in git.
+            Tip today has on the order of
+            <strong>62 PlantUML diagrams</strong>
+            across ~24 chapters (architecture C4, sequences, upgrade flows, 
condition evaluation, scheduler, past-event strategies, and more).</p>
+        <pre class="code-block-light p-3 rounded-3 small 
overflow-auto"><code>[plantuml]
+----
+@startuml
+!include 
https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Component.puml
+Container(unomi, "Apache Unomi")
+Component(restApi, "REST API", "JAX-RS")
+@enduml
+----</code></pre>
+        <p class="small text-muted">Local tip: install GraphViz (<code>brew 
install graphviz</code> / <code>apt-get install graphviz</code>), then
+            <code>cd manual &amp;&amp; mvn -ntp clean install</code>.
+            <code>build.sh</code> also checks for <code>dot</code>.</p>
+
+        <h2 class="h4 mt-5 mb-3">Branded HTML &amp; PDF theme</h2>
+        <p>A later slice of
+            <a href="https://github.com/apache/unomi/pull/821";>#821</a>
+            (UNOMI-960) replaced the default AsciiDoctor chrome with an
+            <strong>Unomi-branded</strong>
+            stylesheet that tracks
+            <a href="https://unomi.apache.org/";>unomi.apache.org</a>:</p>
+        <ul>
+            <li><strong>Design tokens</strong> &mdash; 
<code>--unomi-primary</code> (<code>#6d5ce7</code>), accent purple, Inter 
typography</li>
+            <li><strong>Site nav &amp; footer</strong> via docinfo, version 
badge, hero cover for the HTML book</li>
+            <li><strong>Responsive UX</strong> &mdash; burger nav + bottom TOC 
sheet under 1024px; desktop sidebar TOC from 1024px</li>
+            <li><strong>PDF</strong> &mdash; dedicated 
<code>unomi-theme.yml</code> under <code>manual/src/theme/</code> with matching 
brand assets</li>
+        </ul>
+        <p>Reading the published manual should feel continuous with the 
project website, not like a separate default book skin.</p>
+
+        <h2 class="h4 mt-5 mb-3">Content that matches Unomi 3</h2>
+        <p><a href="https://github.com/apache/unomi/pull/783";>#783</a> synced 
the book to
+            <strong>Apache Unomi 3.x</strong>
+            and expanded (or heavily refreshed) material that operators 
actually need:</p>
+        <ul>
+            <li><strong>Platform story</strong> &mdash; architecture, 
security, configuration, clustering, data structures</li>
+            <li><strong>3.1 product</strong> &mdash; multi-tenancy, task 
scheduler, OpenSearch notes, condition validation / evaluation, property 
types</li>
+            <li><strong>Migrations</strong> &mdash; 2.x&rarr;3.0, 
3.0&rarr;3.1, ES7&rarr;ES9, Elasticsearch&rarr;OpenSearch, V2 compatibility 
mode, script authoring</li>
+            <li><strong>Platform upgrades</strong> &mdash; dedicated 
<code>upgrades/</code> runbooks (Java, Karaf, Elasticsearch, Log4j, 
CXF/Jackson/Jetty, dependency hygiene, ITs)</li>
+            <li><strong>Day-to-day APIs</strong> &mdash; request examples 
(including explain/tracing), GraphQL, shell commands, JavaScript tracker guide, 
recipes</li>
+            <li><strong>Reference depth</strong> &mdash; conditions guide and 
builtin type catalogs (UNOMI-960), past-event conditions, profile tracking</li>
+        </ul>
+        <p>&ldquo;What&rsquo;s new&rdquo; for 3.1 now points readers straight 
into these chapters instead of leaving gaps between release notes and the old 
2.x-shaped manual.</p>
+
+        <h2 class="h4 mt-5 mb-3">How to add a diagram</h2>
+        <ol>
+            <li>Edit the relevant <code>.adoc</code> under 
<code>manual/src/main/asciidoc/</code></li>
+            <li>Add a <code>[plantuml]</code> (or GraphViz) block next to the 
prose it illustrates</li>
+            <li>Build the manual module (GraphViz required for some 
diagrams)</li>
+            <li>Review the generated PNG in the PR like any other source 
change</li>
+        </ol>
+        <p>Website promotional diagrams stay animated SVG / Mermaid on
+            <a href="/blog/">unomi.apache.org</a>;
+            the
+            <strong>manual</strong>
+            sticks to PlantUML&rarr;PNG so AsciiDoctor HTML/PDF stay 
self-contained.</p>
+
+        <p class="mt-4">
+            <a class="btn btn-primary" href="{{ 
site.data.unomi.manual.published_latest }}">Open the latest manual</a>
+            <a class="btn btn-outline-primary ms-2" href="{{ 
site.data.unomi.manual.whats_new }}">What&rsquo;s new in 3.1</a>
+            <a class="btn btn-outline-primary ms-2" 
href="https://github.com/apache/unomi/tree/master/manual/src/main/asciidoc";>Browse
 AsciiDoc on GitHub</a>
+        </p>
+        <p><a href="/blog/">&larr; All posts</a></p>
+    </div>
+</section>
diff --git a/src/main/webapp/blog/unomi-3-openapi-enrichment.html 
b/src/main/webapp/blog/unomi-3-openapi-enrichment.html
new file mode 100644
index 0000000..e400418
--- /dev/null
+++ b/src/main/webapp/blog/unomi-3-openapi-enrichment.html
@@ -0,0 +1,70 @@
+---
+title: Richer OpenAPI docs in Unomi 3.1
+description: "How Unomi 3.1 enriches /cxs/openapi.json from JAX-RS Javadoc — 
summaries, statuses, examples, and schemas without duplicating Swagger 
annotations (UNOMI-963)."
+keywords: "Unomi OpenAPI, openapi.json, UNOMI-963, CXF OpenAPI, Javadoc 
enrichment, Unomi 3.1 REST"
+layout: default
+---
+
+<section class="page-header">
+    <div class="container">
+        <p class="section-label text-hero-label">Unomi 3.1 &middot; Developer 
experience</p>
+        <h1>Richer OpenAPI docs from Javadoc</h1>
+        <p class="lead">Unomi 3.1 ships an enriched 
<code>/cxs/openapi.json</code> &mdash; summaries, status codes, examples, and 
schema notes merged from endpoint Javadoc.</p>
+    </div>
+</section>
+
+<section class="section">
+    <div class="container col-lg-8">
+        <p>Stock CXF OpenAPI generation is a great skeleton and a thin reading 
experience: operation ids, parameter names, and &ldquo;default response.&rdquo;
+            Unomi 3.1 (
+            <a 
href="https://issues.apache.org/jira/browse/UNOMI-963";>UNOMI-963</a>)
+            keeps a <strong>single source of truth in Javadoc</strong> and 
merges richer documentation into the live OpenAPI document at runtime.</p>
+
+        <img src="/assets/images/unomi3/openapi-enrichment.svg" 
class="img-fluid my-4" alt="Javadoc to enrichment pack to CXF openapi.json 
pipeline"/>
+
+        <h2 class="h4 mt-4 mb-3">How the pipeline works</h2>
+        <ol>
+            <li><strong>Build time</strong> &mdash; a Maven Mojo scans JAX-RS 
sources and writes an enrichment pack (<code>openapi-doc.json</code>) into each 
REST module JAR</li>
+            <li><strong>Runtime</strong> &mdash; Unomi&rsquo;s CXF OpenAPI 
customizer loads every pack on the classpath and merges summaries, parameters, 
named statuses, examples, and schema field docs into 
<code>/cxs/openapi.json</code></li>
+        </ol>
+        <p>Authors document once on the Java method / type; agents, Swagger 
UI, and humans all see the same result.</p>
+
+        <h2 class="h4 mt-5 mb-3">Capabilities</h2>
+        <ul>
+            <li><strong>Build-time Mojo</strong> &mdash; scan JAX-RS sources; 
emit enrichment pack JSON into each REST module JAR</li>
+            <li><strong>Multi-pack merge</strong> &mdash; runtime loader 
merges packs from core <code>rest</code> and extension modules</li>
+            <li><strong>CXF OpenApiFeature customizer</strong> &mdash; merges 
into live <code>/cxs/openapi.json</code> (and Swagger UI)</li>
+            <li><strong>Operation summary / description</strong> &mdash; from 
the first Javadoc sentences</li>
+            <li><strong>Parameter docs</strong> &mdash; <code>@param</code> 
Java names remapped to <code>@PathParam</code> / <code>@QueryParam</code> 
values</li>
+            <li><strong>Return / success docs</strong> &mdash; from 
<code>@return</code></li>
+            <li><strong>Named HTTP statuses</strong> &mdash; 
<code>@api.status</code> for realistic 2xx / 4xx / 5xx (not only &ldquo;default 
response&rdquo;)</li>
+            <li><strong>Examples</strong> &mdash; <code>@api.example</code> on 
operations and on schema properties</li>
+            <li><strong>Schema field docs</strong> &mdash; Javadoc on DTO 
fields (enums as string + values; maps document expected keys)</li>
+            <li><strong>Request / response models</strong> &mdash; enrichment 
for types used by hot paths (tenants, context, profiles, rules, segments, 
privacy, extensions, &hellip;)</li>
+            <li><strong>extraSourceRoots</strong> &mdash; document DTOs that 
live in a sibling <code>api</code> module</li>
+            <li><strong>Servlet endpoints</strong> &mdash; optional enrichment 
for documented <code>HttpServlet</code> paths (class-level tags)</li>
+            <li><strong>Single source of truth</strong> &mdash; no duplicate 
Swagger annotation tax on every method</li>
+        </ul>
+
+        <h2 class="h4 mt-5 mb-3">Try it on a running 3.1 server</h2>
+        <pre class="code-block-light p-3 rounded-3 small 
overflow-auto"><code>curl -u karaf:karaf http://localhost:8181/cxs/openapi.json 
| jq '.paths["/tenants/{tenantId}"].get'</code></pre>
+        <p>Compare operation descriptions, response maps, and examples against 
a 2.x or early-3.0 baseline &mdash; the inventory stays stable; the 
documentation density is what changes.</p>
+
+        <h2 class="h4 mt-5 mb-3">Why publish this with 3.1</h2>
+        <p>Unomi 3 kept the REST surface familiar for migrations. Enrichment 
makes that surface
+            <em>discoverable</em>
+            for contributors, customer engineers, and LLM / agent tooling that 
already prefer OpenAPI over tribal knowledge.
+            It pairs with the
+            <a href="/blog/unomi-3-manual.html">manual</a>
+            and
+            <a href="/blog/unomi-3-request-tracing.html">request tracing</a>
+            investments: docs you can read, traces you can run, schemas you 
can generate clients from.</p>
+
+        <p class="mt-4">
+            <a class="btn btn-primary" href="{{ 
site.data.unomi.manual.published_latest }}">Unomi 3.1 manual</a>
+            <a class="btn btn-outline-primary ms-2" 
href="/blog/migrating-to-unomi-3.html">API stability / migrate</a>
+            <a class="btn btn-outline-primary ms-2" 
href="https://issues.apache.org/jira/browse/UNOMI-963";>UNOMI-963</a>
+        </p>
+        <p><a href="/blog/">&larr; All posts</a></p>
+    </div>
+</section>
diff --git a/src/main/webapp/blog/unomi-3-opensearch.html 
b/src/main/webapp/blog/unomi-3-opensearch.html
new file mode 100644
index 0000000..02db882
--- /dev/null
+++ b/src/main/webapp/blog/unomi-3-opensearch.html
@@ -0,0 +1,75 @@
+---
+title: Elasticsearch 9 and OpenSearch in Unomi 3
+description: "Unomi 3 runs on Elasticsearch 9 by default and ships a 
first-class OpenSearch persistence distribution — same SPI, migrate guide, and 
build flags."
+keywords: "Unomi OpenSearch, Elasticsearch 9, Apache Unomi persistence, 
UNOMI-828, migrate Elasticsearch to OpenSearch"
+layout: default
+---
+
+<section class="page-header">
+    <div class="container">
+        <p class="section-label text-hero-label">Unomi 3 &middot; 
Operations</p>
+        <h1>Elasticsearch 9 and OpenSearch in Unomi 3</h1>
+        <p class="lead">One CDP runtime, two supported search backends &mdash; 
pick the distribution that matches your ops constraints.</p>
+    </div>
+</section>
+
+<section class="section">
+    <div class="container col-lg-8">
+        <p>Unomi 3 modernized the search tier on both sides of the fork.
+            Elasticsearch moved to the <strong>9.x Java API client</strong> 
(the legacy High Level REST Client path is gone), and
+            <a 
href="https://issues.apache.org/jira/browse/UNOMI-828";>UNOMI-828</a>
+            /
+            <a href="https://github.com/apache/unomi/pull/715";>#715</a>
+            made <strong>OpenSearch a first-class persistence module</strong> 
with its own distribution packaging.</p>
+
+        <img src="/assets/images/unomi3/opensearch-dual.svg" class="img-fluid 
my-4" alt="Elasticsearch 9 and OpenSearch dual backends sharing Unomi 
PersistenceService SPI"/>
+
+        <h2 class="h4 mt-4 mb-3">Capabilities</h2>
+        <ul>
+            <li><strong>Dual distributions</strong> &mdash; 
<code>unomi-distribution-elasticsearch</code> and 
<code>unomi-distribution-opensearch</code></li>
+            <li><strong>Shared PersistenceService SPI</strong> &mdash; 
profiles, events, segments, rules, clustering, and the task scheduler stay on 
the same application paths</li>
+            <li><strong>Elasticsearch 9 Java API client</strong> &mdash; HLRC 
retired; config prefix <code>org.apache.unomi.elasticsearch.*</code></li>
+            <li><strong>OpenSearch module</strong> &mdash; 
<code>persistence-opensearch</code>; config prefix 
<code>org.apache.unomi.opensearch.*</code> (addresses, cluster name, 
credentials, SSL, index prefix)</li>
+            <li><strong>Build flags</strong> &mdash; Maven / <code>build.sh 
--use-opensearch</code>; IT matrix can run either engine</li>
+            <li><strong>Docker / env</strong> &mdash; 
<code>UNOMI_DISTRIBUTION=...</code> selects packaging</li>
+            <li><strong>Migration guide</strong> &mdash; documented ES &rarr; 
OpenSearch path (index naming, clients, verification)</li>
+            <li><strong>Version pins in 3.1</strong> &mdash; Elasticsearch 
9.4.x line and OpenSearch 3.7.x companion pins in the reactor</li>
+        </ul>
+
+        <h2 class="h4 mt-4 mb-3">What you choose</h2>
+        <ul>
+            <li><strong>Elasticsearch</strong> &mdash; 
<code>persistence-elasticsearch</code>, config prefix 
<code>org.apache.unomi.elasticsearch.*</code> (addresses, cluster name, 
credentials, SSL, index prefix)</li>
+            <li><strong>OpenSearch</strong> &mdash; 
<code>persistence-opensearch</code>, config prefix 
<code>org.apache.unomi.opensearch.*</code> with the same conceptual shape</li>
+        </ul>
+        <p>Both implement the Unomi <code>PersistenceService</code> SPI, so 
profiles, events, segments, rules, clustering, and the task scheduler stay on 
the same application paths.</p>
+
+        <h2 class="h4 mt-5 mb-3">Build and run flavors</h2>
+        <pre class="code-block-light p-3 rounded-3 small 
overflow-auto"><code># Maven / build.sh
+./build.sh
+./build.sh --use-opensearch
+./build.sh --integration-tests --use-opensearch
+
+# Docker distribution hint
+UNOMI_DISTRIBUTION=unomi-distribution-elasticsearch
+UNOMI_DISTRIBUTION=unomi-distribution-opensearch</code></pre>
+
+        <h2 class="h4 mt-5 mb-3">Migrating engines</h2>
+        <p>Moving an existing deployment from Elasticsearch to OpenSearch is 
documented as its own migration path (index naming, client settings, 
verification).
+            Start from
+            <a href="{{ site.data.unomi.manual.es_to_os }}">Migrate from 
Elasticsearch to OpenSearch</a>
+            in the Unomi 3 manual.</p>
+
+        <h2 class="h4 mt-5 mb-3">Why it matters</h2>
+        <p>Regulated and cloud-restricted environments often need an 
OpenSearch footprint; others stay on Elasticsearch.
+            Unomi 3 stops treating that as a fork of the product &mdash; it is 
a packaging and configuration choice on a shared SPI, alongside the
+            <a href="/blog/unomi-3-platform-foundation.html">platform 
foundation</a>
+            upgrade (Java 17, Karaf 4.4, persistence clustering).</p>
+
+        <p class="mt-4">
+            <a class="btn btn-primary" href="{{ 
site.data.unomi.manual.es_to_os }}">Manual: ES to OpenSearch</a>
+            <a class="btn btn-outline-primary ms-2" href="{{ 
site.data.unomi.manual.published_latest }}#_configuration">Manual: 
configuration</a>
+            <a class="btn btn-outline-primary ms-2" 
href="/blog/unomi-3-build-sh.html">build.sh</a>
+        </p>
+        <p><a href="/blog/">&larr; All posts</a></p>
+    </div>
+</section>
diff --git a/src/main/webapp/blog/unomi-3-platform-foundation.html 
b/src/main/webapp/blog/unomi-3-platform-foundation.html
new file mode 100644
index 0000000..cc67d10
--- /dev/null
+++ b/src/main/webapp/blog/unomi-3-platform-foundation.html
@@ -0,0 +1,50 @@
+---
+title: Unomi 3 platform foundation
+description: "What Unomi 3.0 changed under the hood — Java 17, Karaf 4.4, 
Elasticsearch 9, and persistence-based clustering without Cellar or Hazelcast."
+keywords: "Unomi 3 platform, Java 17, Karaf 4.4, UNOMI-877, persistence 
clustering, Elasticsearch 9"
+layout: default
+---
+
+<section class="page-header">
+    <div class="container">
+        <p class="section-label text-hero-label">Unomi 3 &middot; Platform</p>
+        <h1>Unomi 3 platform foundation</h1>
+        <p class="lead">The 3.0 runtime floor &mdash; Java 17, Karaf 4.4, 
Elasticsearch 9, and clustering without Cellar or Hazelcast.</p>
+    </div>
+</section>
+
+<section class="section">
+    <div class="container col-lg-8">
+        <p>Before multi-tenancy and the Unomi 3.1 product leap could stick, 
the project needed a boring, modern runtime.
+            That is what the 3.0 line delivered: fewer legacies, clearer 
installers, and a cluster model that matches how Unomi already stores data.</p>
+
+        <img src="/assets/images/unomi3/platform-foundation.svg" 
class="img-fluid my-4" alt="Unomi 3 platform tiles: Java 17, Karaf 4.4, search 
clients, persistence clustering"/>
+
+        <h2 class="h4 mt-4 mb-3">Baseline upgrades</h2>
+        <ul>
+            <li><strong>Java 17+</strong> &mdash; Java 11 is retired for Unomi 
3 installers and CI</li>
+            <li><strong>Apache Karaf 4.4</strong> &mdash; continued into 3.1 
with current patch lines (for example 4.4.11)</li>
+            <li><strong>Elasticsearch 9</strong> &mdash; Java API client; HLRC 
retired; OpenSearch as a peer distribution (see
+                <a href="/blog/unomi-3-opensearch.html">Elasticsearch 9 and 
OpenSearch</a>)</li>
+            <li><strong>Log / dependency pins</strong> &mdash; platform bump 
PRs keep Log4j and client stacks on supported lines</li>
+        </ul>
+
+        <h2 class="h4 mt-5 mb-3">Persistence-based clustering (UNOMI-877)</h2>
+        <p>Cellar and Hazelcast are gone. Nodes register 
<code>ClusterNode</code> documents in Elasticsearch or OpenSearch, publish 
heartbeats and stats, and discover peers through the shared index.
+            The same mechanism underpins executor selection and locking for the
+            <a href="/blog/unomi-3-task-scheduler.html">cluster-aware task 
scheduler</a>.</p>
+
+        <h2 class="h4 mt-5 mb-3">What operators should do</h2>
+        <p>Treat 3.0 as the platform cutover: confirm Java 17 hosts, pick an 
ES9 or OpenSearch distribution, and read the clustering and health-check 
chapters before stacking tenants and tasks.
+            The
+            <a href="/blog/migrating-to-unomi-3.html">migrating from 2.x</a>
+            post covers the REST and auth cutover; the manual holds the 
day-two runbooks.</p>
+
+        <p class="mt-4">
+            <a class="btn btn-primary" href="{{ 
site.data.unomi.manual.published_latest }}#_clustering">Manual: clustering</a>
+            <a class="btn btn-outline-primary ms-2" href="{{ 
site.data.unomi.manual.whats_new }}">What&rsquo;s new</a>
+            <a class="btn btn-outline-primary ms-2" 
href="/blog/introducing-apache-unomi-3.html">Introducing Unomi 3</a>
+        </p>
+        <p><a href="/blog/">&larr; All posts</a></p>
+    </div>
+</section>
diff --git a/src/main/webapp/blog/unomi-3-request-tracing.html 
b/src/main/webapp/blog/unomi-3-request-tracing.html
new file mode 100644
index 0000000..f5bfdf8
--- /dev/null
+++ b/src/main/webapp/blog/unomi-3-request-tracing.html
@@ -0,0 +1,168 @@
+---
+title: Request tracing with explain=true in Unomi 3
+description: "Debug context and eventcollector requests with explain=true 
&mdash; admin-gated requestTracing trees, verified curl examples, and what 403 
means for public API keys."
+keywords: "Unomi explain, requestTracing, Unomi tracing, UNOMI-873, 
eventcollector explain, context.json explain"
+layout: default
+---
+
+<section class="page-header">
+    <div class="container">
+        <p class="section-label text-hero-label">Unomi 3 &middot; Debugging</p>
+        <h1>Request tracing with <code>explain=true</code></h1>
+        <p class="lead">See condition evaluations, rule actions, and timings 
on <code>/cxs/context.json</code> and <code>/cxs/eventcollector</code>.</p>
+    </div>
+</section>
+
+<section class="section">
+    <div class="container col-lg-8">
+        <p>When a rule &ldquo;should have fired&rdquo; or a session update 
looks wrong, guessing from logs is slow.
+            Unomi 3 adds query-parameter tracing (
+            <a 
href="https://issues.apache.org/jira/browse/UNOMI-873";>UNOMI-873</a>
+            /
+            <a href="https://github.com/apache/unomi/pull/775";>#775</a>):
+            append <code>explain=true</code> and the response includes a 
<code>requestTracing</code> tree.</p>
+
+        <img src="/assets/images/unomi3/request-tracing.svg" class="img-fluid 
my-4" alt="explain=true producing a requestTracing tree of conditions and 
actions"/>
+
+        <h2 class="h4 mt-4 mb-3">Where it works</h2>
+        <ul>
+            <li><code>POST</code>/<code>GET</code> 
<code>/cxs/context.json</code> and <code>/cxs/context.js</code></li>
+            <li><code>POST</code>/<code>GET</code> 
<code>/cxs/eventcollector</code></li>
+        </ul>
+
+        <h2 class="h4 mt-5 mb-3">Capabilities</h2>
+        <ul>
+            <li><strong>Opt-in per request</strong> &mdash; 
<code>?explain=true</code> enables tracing for that call only</li>
+            <li><strong>Response field</strong> &mdash; 
<code>requestTracing</code> on <code>ContextResponse</code> / 
<code>EventCollectorResponse</code></li>
+            <li><strong>Hierarchical tree</strong> &mdash; nested 
<code>TraceNode</code> children for nested work</li>
+            <li><strong>Per-node fields</strong> &mdash; 
<code>operationType</code>, <code>description</code>, <code>context</code>, 
<code>result</code>, <code>startTime</code>, <code>endTime</code>, 
<code>traces[]</code>, <code>children[]</code></li>
+            <li><strong>What gets traced</strong> &mdash; request processing, 
event validation, rule evaluation, condition evaluation 
(property/boolean/&hellip;), action execution (visit counters, segment eval, 
age, &hellip;), personalization / content-filter evaluation on context 
requests</li>
+            <li><strong>Timings</strong> &mdash; wall-clock start/end on every 
node for latency hunting</li>
+            <li><strong>Auth gate</strong> &mdash; 
<code>ROLE_UNOMI_ADMIN</code> / <code>ROLE_UNOMI_TENANT_ADMIN</code> only; 
public API key alone returns HTTP 403</li>
+            <li><strong>Low overhead when off</strong> &mdash; tracer disabled 
unless <code>explain=true</code>; reset after the request</li>
+            <li><strong>Programmatic API</strong> &mdash; 
<code>TracerService</code> / <code>RequestTracer</code> for services that start 
nested operations</li>
+            <li><strong>IT coverage</strong> &mdash; authorized explain 
success + unauthorized 403 paths</li>
+        </ul>
+
+        <h2 class="h4 mt-5 mb-3">Auth: tenant admin, not the public key</h2>
+        <p>Tracing is restricted to <strong>ADMINISTRATOR</strong> / 
<strong>TENANT_ADMINISTRATOR</strong>.
+            In practice that means authenticate with the tenant 
<strong>private</strong> API key (Basic <code>tenantId:privateApiKey</code>), 
not the public track key.</p>
+        <p>We verified the following against a local <strong>Unomi 
3.1.0-SNAPSHOT</strong> (HTTPS). Public-key + <code>explain=true</code> 
returned HTTP <strong>403</strong> with:</p>
+        <pre class="code-block-light p-3 rounded-3 small 
overflow-auto"><code>{"errorMessage":"Insufficient privileges to access tracing 
information"}</code></pre>
+        <p>Integration coverage: 
<code>EventsCollectorIT#testEventsCollectorWithExplain</code> and 
<code>#testEventsCollectorWithExplainUnauthorized</code>.</p>
+
+        <h2 class="h4 mt-5 mb-3">Example: event collector</h2>
+        <pre class="code-block-light p-3 rounded-3 small 
overflow-auto"><code>curl -X POST 
'http://localhost:8181/cxs/eventcollector?explain=true' \
+  -u 'YOUR_TENANT_ID:YOUR_PRIVATE_API_KEY' \
+  -H 'Content-Type: application/json' \
+  -d @- <<'EOF'
+{
+  "sessionId": "trace-demo-1",
+  "events": [
+    {
+      "eventType": "view",
+      "scope": "myscope",
+      "source": {
+        "itemType": "site",
+        "scope": "myscope",
+        "itemId": "mysite"
+      },
+      "target": {
+        "itemType": "page",
+        "scope": "myscope",
+        "itemId": "homepage"
+      }
+    }
+  ]
+}
+EOF</code></pre>
+        <p>Verified excerpt (trimmed) from a live 3.1 response &mdash; session 
create, condition checks, visit counters:</p>
+        <pre class="code-block-light p-3 rounded-3 small overflow-auto"><code>{
+  "updated": 6,
+  "requestTracing": {
+    "operationType": "event-collection",
+    "description": "Event collection request processed successfully",
+    "startTime": 1784099217601,
+    "endTime": 1784099217629,
+    "traces": [
+      "Sending event: sessionCreated - Context: &hellip;",
+      "Processing event for profile: &hellip;"
+    ],
+    "children": [
+      {
+        "operationType": "condition-evaluation",
+        "description": "Condition evaluation completed",
+        "result": "true",
+        "children": [
+          {
+            "operationType": "boolean",
+            "description": "OR condition succeeded on subcondition",
+            "result": "true"
+          }
+        ]
+      },
+      {
+        "operationType": "action-execution",
+        "description": "Action execution completed",
+        "children": [
+          {
+            "operationType": "increment-property",
+            "description": "Property incremented successfully",
+            "traces": [
+              "Processing property increment - Context: 
{propertyName=nbOfVisits, &hellip;}",
+              "Property increment result - Context: {newValue=1, 
isUpdated=true}"
+            ]
+          }
+        ]
+      }
+    ]
+  }
+}</code></pre>
+
+        <h2 class="h4 mt-5 mb-3">Example: context.json</h2>
+        <pre class="code-block-light p-3 rounded-3 small 
overflow-auto"><code>curl -X POST \
+  
'http://localhost:8181/cxs/context.json?sessionId=trace-demo-1&amp;explain=true'
 \
+  -u 'YOUR_TENANT_ID:YOUR_PRIVATE_API_KEY' \
+  -H 'Content-Type: application/json' \
+  -d @- <<'EOF'
+{
+  "source": {
+    "itemId": "homepage",
+    "itemType": "page",
+    "scope": "myscope"
+  },
+  "requiredProfileProperties": ["*"],
+  "requireSegments": true
+}
+EOF</code></pre>
+        <p>Live trace root on the same stack:</p>
+        <pre class="code-block-light p-3 rounded-3 small 
overflow-auto"><code>"requestTracing": {
+  "operationType": "context-request",
+  "description": "Context request processed successfully",
+  "children": [
+    { "operationType": "condition-evaluation", &hellip; },
+    { "operationType": "action-execution",
+      "children": [
+        { "operationType": "evaluate-age", "description": "No changes needed" 
},
+        { "operationType": "evaluate-segments", "description": "No changes 
needed" }
+      ]
+    }
+  ]
+}</code></pre>
+
+        <h2 class="h4 mt-5 mb-3">How to read the tree</h2>
+        <ul>
+            <li><strong>Timings</strong> &mdash; subtract <code>endTime - 
startTime</code> on hot nodes when hunting latency</li>
+            <li><strong>Conditions</strong> &mdash; look for <code>result: 
false</code> on rules you expected to match</li>
+            <li><strong>Actions</strong> &mdash; 
<code>increment-property</code>, <code>evaluate-segments</code>, visit helpers 
show whether profile state actually changed</li>
+        </ul>
+        <p>For save-time condition checks (before runtime), use condition 
validation in the manual; tracing is the runtime companion.</p>
+
+        <p class="mt-4">
+            <a class="btn btn-primary" href="{{ 
site.data.unomi.manual.published_latest }}#_request_tracing_explain">Manual: 
explain / tracing</a>
+            <a class="btn btn-outline-primary ms-2" 
href="/blog/unomi-3-manual.html">Manual improvements</a>
+            <a class="btn btn-outline-primary ms-2" 
href="/blog/unomi-3-task-scheduler.html">Task scheduler</a>
+        </p>
+        <p><a href="/blog/">&larr; All posts</a></p>
+    </div>
+</section>
diff --git a/src/main/webapp/blog/unomi-3-shell-commands.html 
b/src/main/webapp/blog/unomi-3-shell-commands.html
new file mode 100644
index 0000000..1e1b6e8
--- /dev/null
+++ b/src/main/webapp/blog/unomi-3-shell-commands.html
@@ -0,0 +1,116 @@
+---
+title: Unomi 3 Karaf shell commands
+description: "Unomi 3 expands the unomi:* Karaf shell &mdash; unified 
unomi:crud, tenant session context, cache ops, task scheduler commands, and 
list-invalid-objects (UNOMI-879 / 880)."
+keywords: "Unomi shell, unomi:crud, Karaf shell, unomi:cache, unomi:task-list, 
tenant-set, UNOMI-879, UNOMI-880"
+layout: default
+---
+
+<section class="page-header">
+    <div class="container">
+        <p class="section-label text-hero-label">Unomi 3 &middot; Operator 
experience</p>
+        <h1>Karaf shell commands in Unomi 3</h1>
+        <p class="lead"><code>unomi:crud</code>, tenant session context, cache 
tools, and task commands &mdash; operate without leaving SSH.</p>
+    </div>
+</section>
+
+<section class="section">
+    <div class="container col-lg-8">
+        <p>The Unomi Karaf shell (<code>ssh -p 8102 karaf@localhost</code>, 
namespace <code>unomi:</code>) has always been the fastest way to inspect 
rules, tail events, and manage lifecycle.
+            Unomi 3 (
+            <a 
href="https://issues.apache.org/jira/browse/UNOMI-879";>UNOMI-879</a>
+            /
+            <a 
href="https://issues.apache.org/jira/browse/UNOMI-880";>UNOMI-880</a>
+            /
+            <a href="https://github.com/apache/unomi/pull/755";>#755</a>
+            /
+            <a href="https://github.com/apache/unomi/pull/763";>#763</a>)
+            widens that toolkit for multi-tenancy, caches, and the 
cluster-aware scheduler &mdash; with a unified CRUD surface instead of one-off 
list/view/remove commands for every type.</p>
+
+        <img src="/assets/images/unomi3/shell-commands.svg" class="img-fluid 
my-4" alt="Unomi 3 shell: crud, tenant, cache, and task command families"/>
+
+        <h2 class="h4 mt-4 mb-3">Capabilities at a glance</h2>
+        <ul>
+            <li><strong>Unified <code>unomi:crud</code></strong> &mdash; 
<code>create</code> / <code>read</code> / <code>update</code> / 
<code>delete</code> / <code>list</code> / <code>help</code> for many object 
types</li>
+            <li><strong>JSON or file input</strong> &mdash; inline JSON, 
<code>file://</code> paths, or positional id + body patterns</li>
+            <li><strong>Tabular + CSV lists</strong> &mdash; ASCII tables with 
<code>--csv</code> and <code>-n</code> / <code>--max-entries</code></li>
+            <li><strong>Completers</strong> &mdash; operation, type, and id 
completion for faster operator workflows</li>
+            <li><strong>Tenant session</strong> &mdash; 
<code>unomi:tenant-get</code> / <code>unomi:tenant-set</code> so following 
commands run in a tenant (or <code>system</code>)</li>
+            <li><strong>Tenant &amp; API key CRUD</strong> &mdash; 
create/list/update tenants and API keys from the shell</li>
+            <li><strong>Cache management</strong> &mdash; 
<code>unomi:cache</code> stats, reset, clear, inspect, view, remove, watch, 
filters by tenant/type, CSV</li>
+            <li><strong>Scheduler ops</strong> &mdash; 
<code>unomi:task-list|show|cancel|retry|purge|executor</code> (see
+                <a href="/blog/unomi-3-task-scheduler.html">task 
scheduler</a>)</li>
+            <li><strong>Validation assist</strong> &mdash; 
<code>unomi:list-invalid-objects</code> for 
rules/segments/goals/campaigns/scoring with unresolved types</li>
+            <li><strong>Classic live debugging</strong> &mdash; 
<code>rule-tail</code>, <code>rule-watch</code>, <code>event-tail</code>, 
<code>rule-reset-stats</code></li>
+            <li><strong>Lifecycle</strong> &mdash; <code>setup</code>, 
<code>migrate</code>, <code>start</code>, <code>stop</code>, 
<code>version</code></li>
+            <li><strong>Definition deploy</strong> &mdash; 
<code>deploy-definition</code> / <code>undeploy-definition</code> with 
bundle/type wildcards</li>
+            <li><strong>Extension types</strong> &mdash; e.g. JSON Schema via 
<code>unomi:crud ... schema</code> when the extension is installed</li>
+        </ul>
+
+        <h2 class="h4 mt-5 mb-3">Connect and discover</h2>
+        <pre class="code-block-light p-3 rounded-3 small 
overflow-auto"><code>ssh -p 8102 karaf@localhost
+# default user/pass: karaf/karaf  (change etc/users.properties ASAP)
+
+unomi:&lt;TAB&gt;          # list commands
+help unomi:crud
+help unomi:cache</code></pre>
+
+        <h2 class="h4 mt-5 mb-3">Tenant context first</h2>
+        <pre class="code-block-light p-3 rounded-3 small 
overflow-auto"><code>unomi:tenant-set acme
+unomi:tenant-get
+# subsequent CRUD / cache commands use this tenant</code></pre>
+
+        <h2 class="h4 mt-5 mb-3">Unified CRUD</h2>
+        <pre class="code-block-light p-3 rounded-3 small 
overflow-auto"><code>unomi:crud help rule
+unomi:crud list rule --csv -n 50
+unomi:crud read rule my-rule-id
+unomi:crud create rule --file /tmp/rule.json
+unomi:crud update rule my-rule-id -d='{"itemId":"my-rule-id","enabled":false}'
+unomi:crud delete rule my-rule-id
+
+unomi:crud list tenant
+unomi:crud create apikey -d='{"tenantId":"acme","keyType":"PRIVATE"}'
+unomi:crud list profile -n 20
+unomi:crud list schema          # when json-schema extension is 
present</code></pre>
+
+        <h3 class="h6 mt-4 mb-2">Object types registered for 
<code>unomi:crud</code></h3>
+        <p class="small text-muted"><code>actiontype</code>, 
<code>apikey</code>, <code>campaign</code>, <code>campaignevent</code>, 
<code>conditiontype</code>, <code>consent</code>, <code>event</code>, 
<code>goal</code>, <code>persona</code>, <code>profile</code>, 
<code>profilealias</code>, <code>propertytype</code>, <code>rule</code>, 
<code>rulestats</code>, <code>scope</code>, <code>scoring</code>, 
<code>segment</code>, <code>session</code>, <code>tenant</code>, 
<code>topic</code>, pl [...]
+
+        <h2 class="h4 mt-5 mb-3">Cache</h2>
+        <pre class="code-block-light p-3 rounded-3 small 
overflow-auto"><code>unomi:cache --stats
+unomi:cache --stats --detailed --tenant acme
+unomi:cache --stats --type profile --csv
+unomi:cache --clear --tenant acme
+unomi:cache --inspect
+unomi:cache --view --id ENTRY_ID --type &hellip; --tenant acme
+unomi:cache --remove --id ENTRY_ID --type &hellip; --tenant acme
+unomi:cache --watch 5</code></pre>
+
+        <h2 class="h4 mt-5 mb-3">Scheduler (shell twin of REST)</h2>
+        <pre class="code-block-light p-3 rounded-3 small 
overflow-auto"><code>unomi:task-list -s RUNNING
+unomi:task-list -t profile-purge --limit 20
+unomi:task-show TASK_ID
+unomi:task-retry TASK_ID --reset
+unomi:task-cancel TASK_ID
+unomi:task-purge --days 14 --force
+unomi:task-executor</code></pre>
+
+        <h2 class="h4 mt-5 mb-3">Still there: live debugging &amp; 
lifecycle</h2>
+        <ul>
+            <li><code>rule-list|view|remove|reset-stats|tail|watch</code></li>
+            <li><code>event-tail|view|list|search</code></li>
+            <li><code>action-list|view</code>, 
<code>condition-list|view</code></li>
+            <li><code>profile-list|view|remove</code>, <code>segment-*</code>, 
<code>session-*</code></li>
+            <li><code>list-invalid-objects</code></li>
+            <li><code>deploy-definition</code> / 
<code>undeploy-definition</code></li>
+            <li><code>setup</code>, <code>migrate</code>, <code>start</code>, 
<code>stop</code>, <code>version</code></li>
+        </ul>
+        <p>Many day-to-day inspect/edit flows now go through 
<code>unomi:crud</code>; the dedicated list/view/remove commands remain useful 
for scripting habits and live tails.</p>
+
+        <p class="mt-4">
+            <a class="btn btn-primary" href="{{ 
site.data.unomi.manual.published_latest }}#_shell_commands">Manual: shell 
commands</a>
+            <a class="btn btn-outline-primary ms-2" 
href="/blog/unomi-3-task-scheduler.html">Task scheduler</a>
+            <a class="btn btn-outline-primary ms-2" 
href="/blog/unomi-3-multitenancy.html">Multi-tenancy</a>
+        </p>
+        <p><a href="/blog/">&larr; All posts</a></p>
+    </div>
+</section>
diff --git a/src/main/webapp/blog/unomi-3-task-scheduler.html 
b/src/main/webapp/blog/unomi-3-task-scheduler.html
new file mode 100644
index 0000000..47be3ac
--- /dev/null
+++ b/src/main/webapp/blog/unomi-3-task-scheduler.html
@@ -0,0 +1,102 @@
+---
+title: Cluster-aware task scheduler in Unomi 3.1
+description: "Unomi 3.1 durable task scheduler — full capability list: 
persistence, cluster locks, lifecycle, fluent builder, REST ops, shell, 
metrics, resume, and built-in jobs."
+keywords: "Unomi task scheduler, /cxs/tasks, unomi:task-list, Unomi 3.1, 
profile-purge, cluster tasks, TaskExecutor"
+layout: default
+---
+
+<section class="page-header">
+    <div class="container">
+        <p class="section-label text-hero-label">Unomi 3.1 &middot; 
Operations</p>
+        <h1>Cluster-aware task scheduler</h1>
+        <p class="lead">Durable background work with persistence, locks, 
retry/resume, REST, shell, and a plugin-friendly fluent API.</p>
+    </div>
+</section>
+
+<section class="section">
+    <div class="container col-lg-8">
+        <p>Unomi 3.1 introduces a first-class <strong>task scheduler</strong> 
for work that must run reliably on a single node or across a cluster:
+            maintenance timers, request offload (for example profile-merge 
follow-up), and operator-visible retry/cancel.</p>
+        <p>It sits on the
+            <a href="/blog/unomi-3-platform-foundation.html">persistence-based 
clustering</a>
+            model &mdash; executor nodes and locks use the shared search index 
rather than Cellar or Hazelcast.</p>
+
+        <img src="/assets/images/unomi3/task-scheduler.svg" class="img-fluid 
my-4" alt="SchedulerService with REST, built-in task types, and Karaf shell"/>
+
+        <h2 class="h4 mt-4 mb-3">Capabilities at a glance</h2>
+        <ul>
+            <li><strong>Task types + executors</strong> &mdash; each logical 
<code>taskType</code> is handled by a registered <code>TaskExecutor</code> 
(core services or plugins)</li>
+            <li><strong>Persistent or in-memory</strong> &mdash; 
cluster-visible <code>scheduledTask</code> items in Elasticsearch/OpenSearch, 
or <code>nonPersistent()</code> node-local tasks (typical for cache 
refresh)</li>
+            <li><strong>One-shot and recurring</strong> &mdash; 
<code>asOneShot()</code>, or period + <code>withFixedDelay()</code> / 
<code>withFixedRate()</code>, with optional <code>withInitialDelay</code></li>
+            <li><strong>Cluster coordination</strong> &mdash; executor nodes 
(<code>scheduler.executorNode</code>), cluster locks, optional 
<code>runOnAllNodes()</code>, parallel execution control via 
<code>disallowParallelExecution()</code></li>
+            <li><strong>Lifecycle</strong> &mdash; statuses 
<code>SCHEDULED</code>, <code>WAITING</code>, <code>RUNNING</code>, 
<code>COMPLETED</code>, <code>FAILED</code>, <code>CANCELLED</code>, 
<code>CRASHED</code></li>
+            <li><strong>Retries</strong> &mdash; <code>withMaxRetries</code> / 
<code>withRetryDelay</code> (auto); manual retry via REST/shell (optional 
failure-count reset)</li>
+            <li><strong>Dependencies</strong> &mdash; 
<code>withDependencies(taskId&hellip;)</code> so a task waits until prior work 
is <code>COMPLETED</code></li>
+            <li><strong>Checkpoints / resume</strong> &mdash; long jobs save 
<code>checkpointData</code> / <code>currentStep</code>; operators resume 
<code>CRASHED</code> tasks when the executor implements <code>canResume</code> 
/ <code>resume</code></li>
+            <li><strong>Parameters</strong> &mdash; JSON-serializable map 
passed into the executor (include <code>tenantId</code> and run under 
<code>executeAsTenant</code> for tenant isolation)</li>
+            <li><strong>System tasks</strong> &mdash; 
<code>asSystemTask()</code> for jobs created at init that should be preserved 
across restarts</li>
+            <li><strong>Fluent API</strong> &mdash; 
<code>schedulerService.newTask(type)</code> builder; also 
<code>createRecurringTask</code> / <code>createTask</code> helpers</li>
+            <li><strong>Plugin registration</strong> &mdash; 
register/unregister <code>TaskExecutor</code> from OSGi DS 
<code>activate</code> / <code>deactivate</code></li>
+            <li><strong>REST ops</strong> &mdash; list (filter by status/type, 
pagination), get, cancel, retry, resume (admin / tenant-admin); task 
<em>creation</em> stays in Java services/plugins</li>
+            <li><strong>Karaf shell</strong> &mdash; 
<code>unomi:task-list|show|cancel|retry|purge|executor</code></li>
+            <li><strong>Built-in purge</strong> &mdash; periodic 
<code>task-purge</code> of old <code>COMPLETED</code> records 
(<code>scheduler.purgeTaskEnabled</code>, TTL days)</li>
+            <li><strong>Metrics</strong> &mdash; <code>getMetric</code> / 
<code>getAllMetrics</code> (completed, failed, crashed, running, lock 
conflicts, recovery attempts, &hellip;)</li>
+            <li><strong>Config</strong> &mdash; thread pool size, stable 
<code>scheduler.nodeId</code>, lock timeout, executor vs API-only node roles in 
<code>org.apache.unomi.services.cfg</code></li>
+        </ul>
+
+        <h2 class="h4 mt-5 mb-3">REST surface</h2>
+        <p>Base path <code>/cxs/tasks</code> (roles 
<code>ROLE_UNOMI_ADMIN</code> / <code>ROLE_UNOMI_TENANT_ADMIN</code>) &mdash; 
monitoring and operations, not create:</p>
+        <ul>
+            <li><code>GET /cxs/tasks</code> &mdash; paginated list; filters 
<code>status=</code>, <code>type=</code>, <code>offset</code>, 
<code>limit</code></li>
+            <li><code>GET /cxs/tasks/{id}</code> &mdash; detail (parameters, 
checkpoint, errors)</li>
+            <li><code>DELETE /cxs/tasks/{id}</code> &mdash; cancel (HTTP 204; 
record kept as <code>CANCELLED</code>)</li>
+            <li><code>POST /cxs/tasks/{id}/retry</code> &mdash; optional 
<code>?resetFailureCount=true</code></li>
+            <li><code>POST /cxs/tasks/{id}/resume</code> &mdash; for 
<code>CRASHED</code> + checkpoint-capable executors</li>
+        </ul>
+
+        <h2 class="h4 mt-5 mb-3">Karaf shell</h2>
+        <pre class="code-block-light p-3 rounded-3 small 
overflow-auto"><code>unomi:task-list [-s STATUS] [-t TYPE] [--limit N]
+unomi:task-show &lt;id&gt;
+unomi:task-cancel &lt;id&gt;
+unomi:task-retry &lt;id&gt; [--reset]
+unomi:task-purge [--days N] [--force]
+unomi:task-executor          # is this node an executor? nodeId?</code></pre>
+
+        <h2 class="h4 mt-5 mb-3">Fluent scheduling (plugins / services)</h2>
+        <pre class="code-block-light p-3 rounded-3 small 
overflow-auto"><code>schedulerService.newTask("cache-refresh-segment")
+    .nonPersistent()
+    .withPeriod(1, TimeUnit.SECONDS)
+    .withFixedDelay()
+    .disallowParallelExecution()
+    .withSimpleExecutor(() -&gt; refreshSegmentCache())
+    .schedule();
+
+schedulerService.newTask("export-tenant-snapshot")
+    .withParameters(Map.of("tenantId", "acme"))
+    .asOneShot()
+    .withMaxRetries(3)
+    .withRetryDelay(2, TimeUnit.MINUTES)
+    .disallowParallelExecution()
+    .withExecutor(myExportExecutor)
+    .schedule();</code></pre>
+
+        <h2 class="h4 mt-5 mb-3">Built-in task types you will see</h2>
+        <ul>
+            <li><code>cache-refresh-*</code> &mdash; reload segments, rules, 
property types, scopes, &hellip; into memory</li>
+            <li><code>profile-purge</code></li>
+            <li><code>segment-date-recalculation</code></li>
+            <li><code>rules-statistics-refresh</code></li>
+            <li><code>task-purge</code> &mdash; purge old completed scheduler 
tasks</li>
+            <li><code>merge-profiles-reassign-data</code> &mdash; async 
session/event reassignment after profile merge (<code>baseplugin</code>)</li>
+            <li><code>clusterNodeStatisticsUpdate</code> / 
<code>clusterStaleNodesCleanup</code> &mdash; clustering maintenance</li>
+        </ul>
+        <p>Async enrichment-style jobs can reuse the same model instead of 
ad-hoc threads &mdash; register an executor, schedule with the builder, operate 
via REST/shell.</p>
+
+        <p class="mt-4">
+            <a class="btn btn-primary" href="{{ 
site.data.unomi.manual.published_latest }}#_scheduler">Manual: scheduler</a>
+            <a class="btn btn-outline-primary ms-2" 
href="/blog/unomi-3-platform-foundation.html">Platform foundation</a>
+            <a class="btn btn-outline-primary ms-2" 
href="/blog/unomi-3-request-tracing.html">Request tracing</a>
+        </p>
+        <p><a href="/blog/">&larr; All posts</a></p>
+    </div>
+</section>
diff --git a/src/main/webapp/blog/unomi-3-unit-testing-harness.html 
b/src/main/webapp/blog/unomi-3-unit-testing-harness.html
new file mode 100644
index 0000000..9e4135b
--- /dev/null
+++ b/src/main/webapp/blog/unomi-3-unit-testing-harness.html
@@ -0,0 +1,84 @@
+---
+title: Faster Unomi service unit tests — InMemoryPersistenceServiceImpl
+description: "How Unomi 3’s in-memory PersistenceService test harness lets 
Profile, Event, Rules, and Segment services run real unit tests in seconds 
without Karaf or Elasticsearch."
+keywords: "Unomi unit tests, InMemoryPersistenceServiceImpl, UNOMI-881, Unomi 
TestHelper, Surefire"
+layout: default
+---
+
+<section class="page-header">
+    <div class="container">
+        <p class="section-label text-hero-label">Unomi 3 &middot; Quality</p>
+        <h1>Faster service unit tests</h1>
+        <p class="lead"><code>InMemoryPersistenceServiceImpl</code> &mdash; a 
full PersistenceService on the test classpath so service logic no longer waits 
on Karaf.</p>
+    </div>
+</section>
+
+<section class="section">
+    <div class="container col-lg-8">
+        <p>In Unomi 2.x, most confidence meant <strong>integration 
tests</strong>: boot Karaf, talk to Elasticsearch (or Docker), wait.
+            That still matters for the live stack. It is the wrong loop for 
&ldquo;does <code>ProfileServiceImpl</code> handle this edge case?&rdquo;</p>
+        <p>
+            <a 
href="https://issues.apache.org/jira/browse/UNOMI-881";>UNOMI-881</a>
+            /
+            <a href="https://github.com/apache/unomi/pull/772";>#772</a>
+            /
+            <a href="https://github.com/apache/unomi/pull/776";>#776</a>
+            added a unit-test harness centered on one class:</p>
+
+        <pre class="code-block-light p-3 rounded-3 small 
overflow-auto"><code>services/src/test/java/org/apache/unomi/services/impl/InMemoryPersistenceServiceImpl.java</code></pre>
+
+        <img src="/assets/images/unomi3/unit-harness-vs-it.svg" 
class="img-fluid my-4" alt="Unit tests with InMemoryPersistenceServiceImpl 
versus full integration tests"/>
+
+        <h2 class="h4 mt-4 mb-3">What it is (and is not)</h2>
+        <p>It is <strong>not</strong> a production persistence engine. It is a 
complete <code>PersistenceService</code> implementation living only on the 
<strong>test</strong> classpath.</p>
+
+        <h2 class="h4 mt-5 mb-3">Capabilities</h2>
+        <ul>
+            <li><strong>Full SPI surface</strong> &mdash; save/load/remove, 
query (condition + field), range query, paginated <code>PartialList</code>, 
remove-by-query</li>
+            <li><strong>Real condition evaluation</strong> &mdash; 
<code>ConditionEvaluatorDispatcher</code> (not a stubbed query layer)</li>
+            <li><strong>In-memory store</strong> &mdash; 
<code>ConcurrentHashMap</code> keyed items; optional JSON file mirror under 
<code>target/persistence-data</code> (pretty-print, clear-on-init)</li>
+            <li><strong>Refresh simulation</strong> &mdash; optional refresh 
delay thread; per-item-type policies <code>FALSE</code> / <code>TRUE</code> / 
<code>WAIT_FOR</code>; <code>refresh()</code> API</li>
+            <li><strong>Scroll</strong> &mdash; scroll id + time validity like 
ES/OS style paging</li>
+            <li><strong>Default query limit</strong> &mdash; mirrors 
search-engine default size when callers pass negative size</li>
+            <li><strong>Property mappings</strong> &mdash; set/get property 
mapping metadata used by services</li>
+            <li><strong>Aggregates</strong> &mdash; persistence aggregate SPI 
support used by analytics-style service tests</li>
+            <li><strong>Sequences / primary terms</strong> &mdash; index 
sequence helpers for ID generation scenarios</li>
+            <li><strong>System-item ID conventions</strong> &mdash; same 
<code>tenantId_itemId_itemType</code> rules as ES/OS for system types (rules, 
segments, schemas, &hellip;)</li>
+            <li><strong>Tenant transformation listeners</strong> &mdash; 
register listeners like production persistence</li>
+            <li><strong>Batching / overwrite flags</strong> &mdash; 
<code>save(item, useBatching, alwaysOverwrite)</code> overloads</li>
+            <li><strong>Lifecycle</strong> &mdash; <code>shutdown()</code> to 
stop refresh threads between tests</li>
+        </ul>
+
+        <h2 class="h4 mt-5 mb-3">How tests use it</h2>
+        <p>Typical Surefire tests construct the harness, wire a real 
<code>*ServiceImpl</code>, and assert behavior without OSGi:</p>
+        <pre class="code-block-light p-3 rounded-3 small 
overflow-auto"><code>persistenceService = new InMemoryPersistenceServiceImpl(
+        executionContextManager, conditionEvaluatorDispatcher);
+
+// then inject into ProfileServiceImpl, EventServiceImpl,
+// RulesServiceImpl, SegmentServiceImpl, &hellip;</code></pre>
+        <p>Concrete adopters in-tree include 
<code>ProfileServiceImplTest</code>, <code>EventServiceImplTest</code>, 
<code>RulesServiceImplTest</code>, <code>SegmentServiceImplTest</code>, and the 
self-test <code>InMemoryPersistenceServiceImplTest</code>.</p>
+
+        <h2 class="h4 mt-5 mb-3">Unit vs integration</h2>
+        <div class="table-responsive">
+            <table class="table table-sm align-middle">
+                <thead><tr><th></th><th>Integration tests</th><th>Harness unit 
tests</th></tr></thead>
+                <tbody>
+                    <tr><td>Speed</td><td>Minutes+</td><td>Seconds</td></tr>
+                    <tr><td>Isolation</td><td>Shared cluster 
state</td><td>Fresh store per test</td></tr>
+                    <tr><td>Scope</td><td>Full platform</td><td>One service / 
scenario</td></tr>
+                    <tr><td>Fidelity</td><td>Production ES/OS</td><td>SPI + 
conditions + refresh simulation</td></tr>
+                </tbody>
+            </table>
+        </div>
+        <p>ITs prove the live stack (and pair with the
+            <a href="/blog/unomi-3-it-execution.html">clearer IT console</a>).
+            The harness is why Unomi 3 could grow high-signal 
<code>@Test</code> coverage around services without waiting on Docker every 
time.</p>
+
+        <p class="mt-4">
+            <a class="btn btn-primary" 
href="https://github.com/apache/unomi/blob/master/services/src/test/java/org/apache/unomi/services/impl/InMemoryPersistenceServiceImpl.java";>View
 on GitHub</a>
+            <a class="btn btn-outline-primary ms-2" 
href="/contribute/testing.html">Contribute &middot; Testing</a>
+            <a class="btn btn-outline-primary ms-2" 
href="/blog/unomi-3-it-execution.html">IT execution</a>
+        </p>
+        <p><a href="/blog/">&larr; All posts</a></p>
+    </div>
+</section>
diff --git a/src/main/webapp/sitemap.xml b/src/main/webapp/sitemap.xml
index 69901b5..e2030eb 100644
--- a/src/main/webapp/sitemap.xml
+++ b/src/main/webapp/sitemap.xml
@@ -214,6 +214,56 @@
     <changefreq>monthly</changefreq>
     <priority>0.85</priority>
   </url>
+  <url>
+    <loc>https://unomi.apache.org/blog/unomi-3-it-execution.html</loc>
+    <changefreq>monthly</changefreq>
+    <priority>0.75</priority>
+  </url>
+  <url>
+    <loc>https://unomi.apache.org/blog/unomi-3-manual.html</loc>
+    <changefreq>monthly</changefreq>
+    <priority>0.8</priority>
+  </url>
+  <url>
+    <loc>https://unomi.apache.org/blog/unomi-3-build-sh.html</loc>
+    <changefreq>monthly</changefreq>
+    <priority>0.75</priority>
+  </url>
+  <url>
+    <loc>https://unomi.apache.org/blog/unomi-3-platform-foundation.html</loc>
+    <changefreq>monthly</changefreq>
+    <priority>0.8</priority>
+  </url>
+  <url>
+    <loc>https://unomi.apache.org/blog/unomi-3-opensearch.html</loc>
+    <changefreq>monthly</changefreq>
+    <priority>0.8</priority>
+  </url>
+  <url>
+    <loc>https://unomi.apache.org/blog/unomi-3-unit-testing-harness.html</loc>
+    <changefreq>monthly</changefreq>
+    <priority>0.75</priority>
+  </url>
+  <url>
+    <loc>https://unomi.apache.org/blog/unomi-3-shell-commands.html</loc>
+    <changefreq>monthly</changefreq>
+    <priority>0.8</priority>
+  </url>
+  <url>
+    <loc>https://unomi.apache.org/blog/unomi-3-task-scheduler.html</loc>
+    <changefreq>monthly</changefreq>
+    <priority>0.75</priority>
+  </url>
+  <url>
+    <loc>https://unomi.apache.org/blog/unomi-3-request-tracing.html</loc>
+    <changefreq>monthly</changefreq>
+    <priority>0.8</priority>
+  </url>
+  <url>
+    <loc>https://unomi.apache.org/blog/unomi-3-openapi-enrichment.html</loc>
+    <changefreq>monthly</changefreq>
+    <priority>0.8</priority>
+  </url>
   <url>
     <loc>https://unomi.apache.org/llms.txt</loc>
     <lastmod>2026-07-14</lastmod>


Reply via email to