docs: switch to mermaid diagrams

This commit is contained in:
Tero Halla-aho 2025-11-24 22:50:55 +02:00
parent 3a5de63491
commit 810dd71681
17 changed files with 189 additions and 240 deletions

View file

@ -47,6 +47,7 @@
- Soft rejection/removal states for users/listings with timestamps; owner listing removal; login redirects home; listing visibility hides removed/not-published.
- Profile page now allows editing name and password (email immutable).
- Docs: Added docs in `docs/` (tracked, not shipped) with HTML + PlantUML sequences + draw.io diagrams. Ignored from deploy via runtime paths; kept in git.
- Documentation pivoted to Mermaid-only diagrams (sequence + architecture/infra/pipeline), rendered in-browser; legacy draw.io/PlantUML assets removed for simplicity.
- Listing details: right rail now surfaces quick facts + amenity icons; browse map given fixed height so OpenStreetMap tiles show reliably; footer links to privacy page with version indicator.
To resume:

View file

@ -12,25 +12,26 @@
</header>
<main class="grid">
<section class="card">
<h2>Components</h2>
<ul>
<li><strong>Web</strong>: Next.js app (App Router), server-rendered pages, client hooks for auth state.</li>
<li><strong>API routes</strong>: Authentication, admin approvals, listings CRUD (soft-delete), profile update.</li>
<li><strong>Data</strong>: Postgres via Prisma (models: User, Listing, ListingTranslation, ListingImage, VerificationToken).</li>
<li><strong>Mail</strong>: SMTP (smtp.sohva.org) + DKIM signing for verification emails.</li>
<li><strong>Auth</strong>: Email/password, verified+approved requirement, JWT session cookie (<code>session_token</code>), roles.</li>
</ul>
</section>
<section class="card">
<h2>Layers Diagram</h2>
<p>Source: <code>docs/drawio/architecture.drawio</code>. Edit with draw.io and export locally.</p>
</section>
<section class="card">
<h2>Domain Model Snapshot</h2>
<h2>Component map</h2>
<div class="diagram">
<pre><code class="language-mermaid">erDiagram
<pre class="mermaid">
flowchart LR
Browser[Client browser] -->|HTTP/HTTPS| Next[Next.js App Router<br/>SSR/ISR + API routes]
Next --> Prisma[Prisma ORM]
Prisma --> Postgres[(PostgreSQL)]
Next --> Mailer[SMTP mailer<br/>(smtp.sohva.org + DKIM)]
Next --> Storage[Image URLs (remote/bucket)]
Admin[Admin / Moderators] --> Next
Next --> Auth[Auth module<br/>JWT session cookie]
</pre>
</div>
<div class="callout">Edit the Mermaid block above to evolve the architecture.</div>
</section>
<section class="card">
<h2>Domain model</h2>
<div class="diagram">
<pre class="mermaid">erDiagram
USER ||--o{ LISTING : owns
USER ||--o{ LISTING : approves
LISTING ||--|{ LISTINGTRANSLATION : has
@ -67,14 +68,24 @@
string id
string url
}
</code></pre>
</pre>
</div>
</section>
<section class="card">
<h2>Auth Flow (High-Level)</h2>
<p>See PlantUML source: <code>docs/plantuml/auth-register-login.puml</code>. Render locally with PlantUML.</p>
<h2>Key notes</h2>
<ul>
<li><strong>Web</strong>: Next.js app (App Router), server-rendered pages, client hooks for auth state.</li>
<li><strong>API routes</strong>: Authentication, admin approvals, listings CRUD (soft-delete), profile update.</li>
<li><strong>Data</strong>: Postgres via Prisma (models: User, Listing, ListingTranslation, ListingImage, VerificationToken).</li>
<li><strong>Mail</strong>: SMTP (smtp.sohva.org) + DKIM signing for verification emails.</li>
<li><strong>Auth</strong>: Email/password, verified+approved requirement, JWT session cookie (<code>session_token</code>), roles.</li>
</ul>
</section>
</main>
<script type="module">
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs';
mermaid.initialize({ startOnLoad: true, theme: 'dark' });
</script>
</body>
</html>

View file

@ -11,6 +11,26 @@
<div class="meta">Node/Next build, Docker multi-stage, registry push, kubectl rollout.</div>
</header>
<main class="grid">
<section class="card">
<h2>Pipeline at a glance</h2>
<div class="diagram">
<pre class="mermaid">
flowchart LR
Dev[Developer] -->|npm run lint| Lint
Dev --> BuildScript[./deploy/build.sh]
Lint --> BuildScript
BuildScript --> Docker[Docker buildx<br/>multi-stage]
Docker --> Image[registry.halla-aho.net<br/>thalla/lomavuokraus-web]
Image --> Push[./deploy/push.sh]
Push --> DeployStg[./deploy/deploy-staging.sh]
Push --> DeployProd[./deploy/deploy-prod.sh]
DeployStg --> K8sStg[kubectl apply<br/>rollout (staging)]
DeployProd --> K8sProd[kubectl apply<br/>rollout (prod)]
</pre>
</div>
<div class="callout">Edit the Mermaid block to reflect pipeline changes; no external tooling required.</div>
</section>
<section class="card">
<h2>Build Inputs</h2>
<ul>
@ -66,11 +86,10 @@
<li>App env resolution: <code>process.env.*</code> in Next server code.</li>
</ul>
</section>
<section class="card">
<h2>Pipeline Diagram</h2>
<p>For visuals, edit/export <code>docs/drawio/architecture.drawio</code> or create a dedicated pipeline page in draw.io.</p>
</section>
</main>
<script type="module">
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs';
mermaid.initialize({ startOnLoad: true, theme: 'dark' });
</script>
</body>
</html>

View file

@ -1,52 +0,0 @@
<mxfile host="app.diagrams.net">
<diagram id="architecture" name="Architecture">
<mxGraphModel dx="923" dy="570" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1200" pageHeight="900" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="browser" value="Browser" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#26A69A;fontColor=#ffffff" vertex="1" parent="1">
<mxGeometry x="80" y="80" width="120" height="50" as="geometry" />
</mxCell>
<mxCell id="next" value="Next.js App" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#3949AB;fontColor=#ffffff" vertex="1" parent="1">
<mxGeometry x="260" y="80" width="140" height="60" as="geometry" />
</mxCell>
<mxCell id="apiRoutes" value="API Routes (Auth, Listings, Admin)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#5E35B1;fontColor=#ffffff" vertex="1" parent="1">
<mxGeometry x="260" y="170" width="200" height="60" as="geometry" />
</mxCell>
<mxCell id="prisma" value="Prisma Client" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#00897B;fontColor=#ffffff" vertex="1" parent="1">
<mxGeometry x="520" y="170" width="140" height="60" as="geometry" />
</mxCell>
<mxCell id="db" value="PostgreSQL" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#8D6E63;fontColor=#ffffff" vertex="1" parent="1">
<mxGeometry x="720" y="170" width="140" height="60" as="geometry" />
</mxCell>
<mxCell id="smtp" value="SMTP / DKIM" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#F4511E;fontColor=#ffffff" vertex="1" parent="1">
<mxGeometry x="720" y="270" width="140" height="60" as="geometry" />
</mxCell>
<mxCell id="cookie" value="JWT session_token" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#0097A7;fontColor=#ffffff" vertex="1" parent="1">
<mxGeometry x="480" y="80" width="140" height="50" as="geometry" />
</mxCell>
<mxCell id="flow1" edge="1" source="browser" target="next" style="endArrow=block;strokeColor=#26A69A" parent="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="flow2" edge="1" source="next" target="apiRoutes" style="endArrow=block;strokeColor=#3949AB" parent="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="flow3" edge="1" source="apiRoutes" target="prisma" style="endArrow=block;strokeColor=#5E35B1" parent="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="flow4" edge="1" source="prisma" target="db" style="endArrow=block;strokeColor=#00897B" parent="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="flow5" edge="1" source="apiRoutes" target="smtp" style="endArrow=block;strokeColor=#F4511E" parent="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="flow6" edge="1" source="next" target="cookie" style="endArrow=block;strokeColor=#0097A7" parent="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="flow7" edge="1" source="cookie" target="browser" style="endArrow=block;strokeColor=#0097A7" parent="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>

View file

@ -1,64 +0,0 @@
<mxfile host="app.diagrams.net">
<diagram id="infra" name="Infrastructure">
<mxGraphModel dx="923" dy="570" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1200" pageHeight="900" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="cluster" value="k3s Cluster" style="swimlane;childLayout=stackLayout;horizontal=1;startSize=20;" vertex="1" parent="1">
<mxGeometry x="140" y="140" width="700" height="420" as="geometry" />
</mxCell>
<mxCell id="traefik" value="Traefik Ingress" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#1E88E5;fontColor=#ffffff" vertex="1" parent="cluster">
<mxGeometry x="40" y="60" width="140" height="60" as="geometry" />
</mxCell>
<mxCell id="svc" value="Service: lomavuokraus-web" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#3949AB;fontColor=#ffffff" vertex="1" parent="cluster">
<mxGeometry x="240" y="60" width="180" height="60" as="geometry" />
</mxCell>
<mxCell id="pod" value="Deployment (Next.js pods)" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#5E35B1;fontColor=#ffffff" vertex="1" parent="cluster">
<mxGeometry x="480" y="60" width="180" height="60" as="geometry" />
</mxCell>
<mxCell id="cm" value="ConfigMap + Secret" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#00897B;fontColor=#ffffff" vertex="1" parent="cluster">
<mxGeometry x="480" y="170" width="180" height="50" as="geometry" />
</mxCell>
<mxCell id="cert" value="cert-manager&#xa;ClusterIssuers" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#00838F;fontColor=#ffffff" vertex="1" parent="cluster">
<mxGeometry x="240" y="170" width="180" height="50" as="geometry" />
</mxCell>
<mxCell id="browser" value="Browser" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#26A69A;fontColor=#ffffff" vertex="1" parent="1">
<mxGeometry x="40" y="70" width="100" height="50" as="geometry" />
</mxCell>
<mxCell id="registry" value="Registry&#xa;registry.halla-aho.net" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#6D4C41;fontColor=#ffffff" vertex="1" parent="1">
<mxGeometry x="40" y="340" width="150" height="60" as="geometry" />
</mxCell>
<mxCell id="db" value="PostgreSQL&#xa;46.62.203.202" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#8D6E63;fontColor=#ffffff" vertex="1" parent="1">
<mxGeometry x="860" y="220" width="160" height="60" as="geometry" />
</mxCell>
<mxCell id="smtp" value="SMTP&#xa;smtp.sohva.org" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#F4511E;fontColor=#ffffff" vertex="1" parent="1">
<mxGeometry x="860" y="320" width="160" height="60" as="geometry" />
</mxCell>
<mxCell id="registry-edge" edge="1" source="registry" target="pod" style="endArrow=block;strokeColor=#6D4C41" parent="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="browser-edge" edge="1" source="browser" target="traefik" style="endArrow=block;strokeColor=#26A69A" parent="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="traefik-svc-edge" edge="1" source="traefik" target="svc" style="endArrow=block;strokeColor=#1E88E5" parent="cluster">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="svc-pod-edge" edge="1" source="svc" target="pod" style="endArrow=block;strokeColor=#3949AB" parent="cluster">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="pod-db-edge" edge="1" source="pod" target="db" style="endArrow=block;strokeColor=#8D6E63" parent="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="pod-smtp-edge" edge="1" source="pod" target="smtp" style="endArrow=block;strokeColor=#F4511E" parent="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="cert-traefik-edge" edge="1" source="cert" target="traefik" style="endArrow=block;strokeColor=#00838F" parent="cluster">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="cm-pod-edge" edge="1" source="cm" target="pod" style="endArrow=block;strokeColor=#00897B" parent="cluster">
<mxGeometry relative="1" as="geometry" />
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>

View file

@ -8,10 +8,10 @@
<body>
<header>
<h1>Lomavuokraus Documentation</h1>
<div class="meta">Docs tracked in git, not deployed with the app.</div>
<div class="meta">Diagram-first docs rendered with Mermaid; tracked in git, not deployed with the app.</div>
</header>
<main class="grid">
<div class="card">
<section class="card">
<h2>Contents</h2>
<ul>
<li><a href="./infra.html">Infrastructure</a></li>
@ -19,16 +19,19 @@
<li><a href="./architecture.html">Logical Architecture</a></li>
<li><a href="./sequences.html">Feature Sequences</a></li>
</ul>
</div>
<div class="card">
<h3>Notes</h3>
</section>
<section class="card">
<h3>How diagrams work</h3>
<ul>
<li>Docs live in <code>docs/</code> (tracked, not shipped).</li>
<li>Sequence diagrams: PlantUML sources in <code>docs/plantuml</code>.</li>
<li>Architecture/infra diagrams: draw.io sources in <code>docs/drawio</code>.</li>
<li>Generate locally: PlantUML CLI/JAR, draw.io desktop/CLI exports.</li>
<li>All diagrams use Mermaid (rendered in-browser via CDN; no extra tooling needed).</li>
<li>Graph definitions live inline in the HTML for quick edits.</li>
<li>Docs live in <code>docs/</code>; they are tracked but not shipped with the app.</li>
</ul>
</div>
</section>
</main>
<script type="module">
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs';
mermaid.initialize({ startOnLoad: true, theme: 'dark' });
</script>
</body>
</html>

View file

@ -8,11 +8,35 @@
<body>
<header>
<h1>Infrastructure Overview</h1>
<div class="meta">
Hetzner k3s cluster, Traefik ingress, cert-manager TLS, private registry, staging/prod namespaces.
</div>
<div class="meta">Hetzner k3s cluster, Traefik ingress, cert-manager TLS, private registry, staging/prod namespaces.</div>
</header>
<main class="grid">
<section class="card">
<h2>Traffic flow</h2>
<div class="diagram">
<pre class="mermaid">
graph LR
User[User Browser] -->|HTTPS| Traefik[Traefik Ingress<br/>IngressClass traefik]
Traefik -->|Host: lomavuokraus.fi<br/>staging.lomavuokraus.fi| Service[Service<br/>port 80 -> 3000]
Service --> Pod[Next.js Pods (2)]
Pod --> DB[(PostgreSQL<br/>46.62.203.202)]
Pod --> SMTP[smtp.sohva.org]
subgraph Cluster [k3s Cluster hel1 cx22 157.180.66.64]
Traefik
Service
Pod
CertMgr[cert-manager]
Secret[Secrets: lomavuokraus-web-secrets]
CM[ConfigMap: lomavuokraus-web-config]
end
CertMgr -->|TLS| Traefik
Registry[registry.halla-aho.net/thalla/lomavuokraus-web] -->|pull| Pod
DNS[lomavuokraus.fi<br/>staging.lomavuokraus.fi<br/>api.lomavuokraus.fi] --> Traefik
</pre>
</div>
<div class="callout">Mermaid renders directly in the browser; edit the graph in this file to update.</div>
</section>
<section class="card">
<h2>Cluster &amp; Namespaces</h2>
<ul>
@ -64,11 +88,10 @@
<li>Session auth: signed JWT cookie <code>session_token</code>; roles: USER, ADMIN, USER_MODERATOR, LISTING_MODERATOR.</li>
</ul>
</section>
<section class="card">
<h2>Traffic Flow Diagram</h2>
<p>Source: <code>docs/drawio/infra.drawio</code> (edit with draw.io, export PNG locally).</p>
</section>
</main>
<script type="module">
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs';
mermaid.initialize({ startOnLoad: true, theme: 'dark' });
</script>
</body>
</html>

View file

@ -1,19 +0,0 @@
@startuml
title User registration, verification, login, approval
actor User
participant "Next API" as API
database Postgres as DB
participant SMTP as Mail
actor Admin
User -> API: POST /api/auth/register\n(email, password, name)
API -> DB: create User (status=PENDING)\ncreate VerificationToken
API -> Mail: send verification email
User -> API: POST /api/auth/verify (token)
API -> DB: set emailVerifiedAt
Admin -> API: POST /api/admin/users/approve
API -> DB: set status=ACTIVE, approvedAt
User -> API: POST /api/auth/login
API -> DB: validate password + status
API --> User: session_token cookie (JWT)
@enduml

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 12 KiB

View file

@ -1,18 +0,0 @@
@startuml
title Listing creation and admin approval
actor Owner
participant "Next API" as API
database Postgres as DB
actor Admin
actor User
Owner -> API: POST /api/listings\n(slug, details, images)
API -> DB: create Listing\nstatus=PENDING (or PUBLISHED if auto)
Admin -> API: GET /api/admin/pending
API -> DB: fetch pending listings
Admin -> API: POST /api/admin/listings/approve\n(action=approve|reject|remove)
API -> DB: update status, timestamps
User -> API: GET /listings/[slug]
API -> DB: fetch translation\nstatus=PUBLISHED and not removed
API --> User: render listing
@enduml

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 12 KiB

View file

@ -1,18 +0,0 @@
@startuml
title Listing removal by owner or moderator
actor Owner
participant "Next API" as API
database Postgres as DB
actor Moderator
Owner -> API: POST /api/listings/remove (listingId)
API -> DB: verify owner or moderator\nfetch listing
API -> DB: set status=REMOVED\npublished=false\nremovedAt/by
Moderator -> API: POST /api/admin/listings/approve\n(action=remove)
API -> DB: same status change if via admin path
Owner -> API: GET /api/listings/mine
API -> DB: fetch listings for owner (includes REMOVED)
Public -> API: GET /listings/[slug]
API -> DB: filter out removed listings
API --> Public: not found if removed
@enduml

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 12 KiB

View file

@ -1,10 +0,0 @@
@startuml
title Profile update (name/password)
actor User
participant "Next API" as API
database Postgres as DB
User -> API: PATCH /api/me\n(name?, password?)
API -> DB: update name/passwordHash\n(email immutable)
API --> User: updated profile payload
@enduml

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 6.3 KiB

View file

@ -8,40 +8,102 @@
<body>
<header>
<h1>Sequence Diagrams</h1>
<div class="meta">PlantUML sources for key flows; render locally.</div>
<div class="meta">Mermaid-rendered flows for the most important user journeys.</div>
</header>
<main class="grid">
<section class="card">
<h2>User Registration &amp; Verification</h2>
<p>PlantUML: <code>docs/plantuml/auth-register-login.puml</code></p>
<img src="./plantuml/auth-register-login.svg" alt="User registration and verification sequence" class="diagram-img" />
<div class="diagram">
<pre class="mermaid">
sequenceDiagram
participant U as User
participant W as Web (Next.js)
participant DB as Postgres
participant Mail as SMTP
U->>W: Submit registration form (email, password)
W->>DB: Create pending user + verification token
W->>Mail: Send verification email with token link
U-->>Mail: Opens email
U->>W: Click verify link
W->>DB: Mark email verified (status = verified, awaiting approval)
Admin->>W: Approves user
W->>DB: Update status = approved
U->>W: Login (email/password)
W->>DB: Validate credentials, create session token
W-->>U: Set session cookie
</pre>
</div>
</section>
<section class="card">
<h2>Listing Creation &amp; Approval</h2>
<p>PlantUML: <code>docs/plantuml/listing-create-approve.puml</code></p>
<img src="./plantuml/listing-create-approve.svg" alt="Listing creation and approval sequence" class="diagram-img" />
<div class="diagram">
<pre class="mermaid">
sequenceDiagram
participant Host as Host (logged in)
participant W as Web/API
participant DB as Postgres
participant Mod as Moderator
Host->>W: Open "New listing" and submit details (address, amenities, images)
W->>DB: Save listing (status = PENDING)
Mod->>W: Review pending listing
W->>DB: Approve -> status = PUBLISHED
Host-->>W: Listing appears in "My listings" and public browse
Public->>W: Browse latest/map; fetch listing translation + cover image
</pre>
</div>
</section>
<section class="card">
<h2>Listing Removal by Owner/Moderator</h2>
<p>PlantUML: <code>docs/plantuml/listing-removal.puml</code></p>
<img src="./plantuml/listing-removal.svg" alt="Listing removal sequence" class="diagram-img" />
<div class="diagram">
<pre class="mermaid">
sequenceDiagram
participant Owner as Owner/Moderator
participant W as Web/API
participant DB as Postgres
Owner->>W: Click "Remove" on listing
W->>Owner: Confirm removal
Owner-->>W: Confirm
W->>DB: Set removedAt timestamp, status = REMOVED
W-->>Owner: Show updated state in "My listings"
Public--xW: Listing hidden from browse/slug pages
</pre>
</div>
</section>
<section class="card">
<h2>Profile Update (Name/Password)</h2>
<p>PlantUML: <code>docs/plantuml/profile-update.puml</code></p>
<img src="./plantuml/profile-update.svg" alt="Profile update sequence" class="diagram-img" />
<div class="diagram">
<pre class="mermaid">
sequenceDiagram
participant User
participant W as Web/API
participant DB as Postgres
User->>W: Open profile page (requires session cookie)
W->>DB: Fetch user record (email immutable)
User->>W: Submit updated name and/or password
W->>DB: Update fields (hash password if provided)
W-->>User: Show "Profile updated"
</pre>
</div>
</section>
<section class="card">
<h2>Rendering instructions</h2>
<ul>
<li>PlantUML: <code>plantuml docs/plantuml/*.puml</code> (local PlantUML/Java or Docker).</li>
<li>Draw.io: open <code>docs/drawio/*.drawio</code> in the desktop app to edit/export PNG/SVG locally.</li>
<li>Mermaid renders automatically in-browser via CDN; no local tooling required.</li>
<li>Edit the Mermaid blocks inline in these HTML files.</li>
</ul>
</section>
</main>
<script type="module">
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs';
mermaid.initialize({ startOnLoad: true, theme: 'dark' });
</script>
</body>
</html>

View file

@ -80,3 +80,18 @@ ul {
border-radius: 10px;
background: #0b1220;
}
.mermaid {
background: #0b1220;
border: 1px solid #1f2937;
border-radius: 12px;
padding: 12px;
}
.callout {
background: #0b1220;
border: 1px dashed #1f2937;
border-radius: 12px;
padding: 12px;
color: #cbd5e1;
}