docs: move docs directory and render plantuml svgs

This commit is contained in:
Tero Halla-aho 2025-11-24 17:31:21 +02:00
parent 590daacb76
commit 66650d63ac
18 changed files with 583 additions and 4 deletions

4
.gitignore vendored
View file

@ -20,8 +20,7 @@ deploy/.last-image
creds/
k3s.yaml
# Local-only documentation
docs-local/
# Local-only documentation (now tracked in docs/)
/lib/generated/prisma
@ -29,6 +28,7 @@ docs-local/
bin/
lib/python*/
lib64/
lib64
pyvenv.cfg
tsconfig.tsbuildinfo
.lock

View file

@ -39,13 +39,14 @@
- Amenities expanded: electric vehicle charging (free/paid) and air conditioning; cover image selectable per listing and used in cards.
- Home page shows a rolling feed of latest listings; navbar + CTA link to browse.
- Listing creation form captures address details, coordinates, amenities (incl. EV/AC), and cover image choice.
- Documentation moved to `docs/`; PlantUML diagrams rendered to SVG and embedded in docs pages (draw.io sources kept for architecture/infra).
- HTTPS redirect middleware applied to staging/prod ingress.
- FI/EN localization with navbar language toggle; UI strings translated; Approvals link shows pending count badge.
- 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 local docs in `docs-local/` (tracked, not shipped) with HTML + PlantUML sequences + draw.io diagrams. Ignored from deploy via runtime paths; kept in git.
- Docs: Added docs in `docs/` (tracked, not shipped) with HTML + PlantUML sequences + draw.io diagrams. Ignored from deploy via runtime paths; kept in git.
To resume:
1) If desired, render diagrams locally: PlantUML in `docs-local/plantuml`, draw.io in `docs-local/drawio`.
1) If desired, render diagrams locally: PlantUML in `docs/plantuml`, draw.io in `docs/drawio`.
2) Keep registry health in mind; current pushes work (`1763994382` deployed).
3) Future app work: translations polish, more listing fields, admin tooling, or registry hardening.

80
docs/architecture.html Normal file
View file

@ -0,0 +1,80 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Logical Architecture</title>
<link rel="stylesheet" href="./style.css" />
</head>
<body>
<header>
<h1>Logical Architecture</h1>
<div class="meta">Next.js App Router, Prisma/Postgres, role-based auth, email verification, approvals.</div>
</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>
<div class="diagram">
<pre><code class="language-mermaid">erDiagram
USER ||--o{ LISTING : owns
USER ||--o{ LISTING : approves
LISTING ||--|{ LISTINGTRANSLATION : has
LISTING ||--o{ LISTINGIMAGE : has
USER {
string id
string email
string passwordHash
Role role
UserStatus status
datetime emailVerifiedAt
datetime approvedAt
datetime rejectedAt
datetime removedAt
}
LISTING {
string id
ListingStatus status
datetime approvedAt
datetime rejectedAt
datetime removedAt
string country
string region
string city
}
LISTINGTRANSLATION {
string id
string slug
string title
string locale
}
LISTINGIMAGE {
string id
string url
}
</code></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>
</section>
</main>
</body>
</html>

76
docs/build.html Normal file
View file

@ -0,0 +1,76 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Build & Deploy</title>
<link rel="stylesheet" href="./style.css" />
</head>
<body>
<header>
<h1>Build &amp; Deploy Pipeline</h1>
<div class="meta">Node/Next build, Docker multi-stage, registry push, kubectl rollout.</div>
</header>
<main class="grid">
<section class="card">
<h2>Build Inputs</h2>
<ul>
<li>Source: Next.js app with TypeScript and Prisma.</li>
<li>Env: <code>.env</code> (local), K8s Secret <code>lomavuokraus-web-secrets</code> in cluster.</li>
<li>Prisma schema: <code>prisma/schema.prisma</code>, migrations in <code>prisma/migrations/</code>.</li>
</ul>
</section>
<section class="card">
<h2>NPM Scripts</h2>
<ul>
<li><code>npm run lint</code><code>next lint</code></li>
<li><code>npm run build</code><code>next build</code> (used inside Docker and locally)</li>
</ul>
</section>
<section class="card">
<h2>Docker Image</h2>
<ul>
<li>Multi-stage Dockerfile:
<ul>
<li>deps: npm ci</li>
<li>builder: copy source, <code>npx prisma generate</code>, <code>npm run build</code></li>
<li>runner: Node 20 bookworm-slim, copy standalone + static</li>
</ul>
</li>
<li>Tags: numeric (git SHA-derived) + <code>:latest</code>.</li>
<li>Scan: Trivy runs post-build if available.</li>
</ul>
</section>
<section class="card">
<h2>Deploy Scripts</h2>
<ul>
<li><code>deploy/build.sh</code> → build image, write <code>deploy/.last-image</code>.</li>
<li><code>deploy/push.sh</code> → push image.</li>
<li><code>deploy/deploy.sh</code> → envsubst <code>k8s/app.yaml</code>, kubectl apply, rollout.</li>
<li>Environment wrappers:
<ul>
<li><code>deploy/deploy-staging.sh</code></li>
<li><code>deploy/deploy-prod.sh</code></li>
</ul>
</li>
</ul>
</section>
<section class="card">
<h2>Config & Env Vars</h2>
<ul>
<li>From ConfigMap (public): <code>NEXT_PUBLIC_SITE_URL</code>, <code>NEXT_PUBLIC_API_BASE</code>, <code>APP_ENV</code>.</li>
<li>From Secret: DB URL, AUTH_SECRET, SMTP, DKIM, etc.</li>
<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>
</body>
</html>

View file

@ -0,0 +1,52 @@
<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>

64
docs/drawio/infra.drawio Normal file
View file

@ -0,0 +1,64 @@
<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>

34
docs/index.html Normal file
View file

@ -0,0 +1,34 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Lomavuokraus Docs</title>
<link rel="stylesheet" href="./style.css" />
</head>
<body>
<header>
<h1>Lomavuokraus Documentation</h1>
<div class="meta">Docs tracked in git, not deployed with the app.</div>
</header>
<main class="grid">
<div class="card">
<h2>Contents</h2>
<ul>
<li><a href="./infra.html">Infrastructure</a></li>
<li><a href="./build.html">Build &amp; Deploy</a></li>
<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>
<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>
</ul>
</div>
</main>
</body>
</html>

74
docs/infra.html Normal file
View file

@ -0,0 +1,74 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Infrastructure</title>
<link rel="stylesheet" href="./style.css" />
</head>
<body>
<header>
<h1>Infrastructure Overview</h1>
<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>Cluster &amp; Namespaces</h2>
<ul>
<li>Single-node k3s (Hetzner hel1 cx22) at <code>157.180.66.64</code>.</li>
<li>Namespaces: <code>lomavuokraus-prod</code>, <code>lomavuokraus-staging</code>.</li>
<li>Ingress controller: Traefik (k3s default).</li>
<li>cert-manager v1.15.3 with ClusterIssuers:
<ul>
<li><code>letsencrypt-prod</code> (ACME prod)</li>
<li><code>letsencrypt-staging</code> (ACME staging for test certs)</li>
</ul>
</li>
<li>DNS: <code>lomavuokraus.fi</code>, <code>staging.lomavuokraus.fi</code>, <code>api.lomavuokraus.fi</code> -> cluster IP.</li>
</ul>
</section>
<section class="card">
<h2>Registry</h2>
<ul>
<li>Private registry: <code>registry.halla-aho.net/thalla/lomavuokraus-web</code>.</li>
<li>Credentials stored outside repo (<code>creds/</code>), image pull secret <code>registry-halla</code> in staging/prod namespaces.</li>
<li>Images tagged with git SHA-derived numeric tag and <code>:latest</code>.</li>
</ul>
</section>
<section class="card">
<h2>App Manifests</h2>
<ul>
<li><code>k8s/app.yaml</code> templated via envsubst in deploy scripts.</li>
<li>Objects:
<ul>
<li>ConfigMap: <code>lomavuokraus-web-config</code> (public env).</li>
<li>Deployment: 2 replicas, container port 3000, liveness/readiness on <code>/api/health</code>.</li>
<li>Service: ClusterIP on port 80.</li>
<li>Ingress: Traefik class, TLS via cert-manager, HTTPS redirect middleware.</li>
<li>Traefik Middleware: <code>https-redirect</code> to force HTTPS.</li>
</ul>
</li>
<li>Secrets: <code>lomavuokraus-web-secrets</code> in cluster (not in repo).</li>
</ul>
</section>
<section class="card">
<h2>Runtime Environment</h2>
<ul>
<li>Next.js 14.2.33 (App Router) running via Node.js 20 in Docker.</li>
<li>PostgreSQL DB at <code>46.62.203.202</code> (DATABASE_URL in .env, not committed).</li>
<li>SMTP: smtp.sohva.org, DKIM key under <code>creds/dkim/...</code>.</li>
<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>
</body>
</html>

View file

@ -0,0 +1,19 @@
@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

After

Width:  |  Height:  |  Size: 12 KiB

View file

@ -0,0 +1,18 @@
@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

After

Width:  |  Height:  |  Size: 12 KiB

View file

@ -0,0 +1,18 @@
@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

After

Width:  |  Height:  |  Size: 12 KiB

View file

@ -0,0 +1,10 @@
@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

After

Width:  |  Height:  |  Size: 6.3 KiB

47
docs/sequences.html Normal file
View file

@ -0,0 +1,47 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Feature Sequences</title>
<link rel="stylesheet" href="./style.css" />
</head>
<body>
<header>
<h1>Sequence Diagrams</h1>
<div class="meta">PlantUML sources for key flows; render locally.</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" />
</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" />
</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" />
</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" />
</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>
</ul>
</section>
</main>
</body>
</html>

82
docs/style.css Normal file
View file

@ -0,0 +1,82 @@
body {
font-family: "Inter", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
margin: 0;
padding: 0;
background: #0f172a;
color: #e2e8f0;
}
a {
color: #38bdf8;
}
header {
padding: 24px 32px;
background: linear-gradient(135deg, #1e293b, #0f172a 60%);
border-bottom: 1px solid #1f2937;
}
h1,
h2,
h3 {
color: #f8fafc;
}
main {
padding: 24px 32px 48px;
display: grid;
gap: 16px;
max-width: 1200px;
}
.card {
background: #111827;
border: 1px solid #1f2937;
border-radius: 14px;
padding: 20px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.25);
}
.meta {
color: #cbd5e1;
font-size: 14px;
}
.grid {
display: grid;
gap: 12px;
}
.two-col {
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
}
pre {
background: #0b1220;
padding: 12px;
border-radius: 10px;
overflow-x: auto;
border: 1px solid #1f2937;
}
code {
font-family: "SFMono-Regular", Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
color: #cbd5e1;
}
.diagram {
background: #0b1220;
border: 1px solid #1f2937;
border-radius: 12px;
padding: 12px;
}
.badge {
display: inline-block;
background: #38bdf8;
color: #0f172a;
padding: 4px 8px;
border-radius: 999px;
font-weight: 600;
font-size: 12px;
}
ul {
padding-left: 18px;
}
.diagram-img {
width: 100%;
max-width: 960px;
margin-top: 10px;
border: 1px solid #1f2937;
border-radius: 10px;
background: #0b1220;
}