docs(reports): backup from typst online
Signed-off-by: Klagarge <remi@heredero.ch>
This commit is contained in:
@@ -10,6 +10,28 @@
|
|||||||
// mqtt->db
|
// mqtt->db
|
||||||
// db->rest
|
// db->rest
|
||||||
|
|
||||||
|
|
||||||
|
== Database & API - Server
|
||||||
|
#let server = [
|
||||||
|
#figure(
|
||||||
|
image("server.png", width: 100%),
|
||||||
|
caption: [],
|
||||||
|
) <fig:server>
|
||||||
|
]
|
||||||
|
#grid(
|
||||||
|
columns: (1.5fr, 1fr),
|
||||||
|
column-gutter: 2em,
|
||||||
|
server,
|
||||||
|
align(top+left)[
|
||||||
|
#v(3em) #pause
|
||||||
|
- LXC Debian #pause
|
||||||
|
- @ssh:short certificate by user #pause
|
||||||
|
- Docker #pause
|
||||||
|
- Traefik
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
== Database & API - Save measures in DB
|
||||||
#let toDB = {
|
#let toDB = {
|
||||||
import chronos: *
|
import chronos: *
|
||||||
b.display
|
b.display
|
||||||
|
|||||||
@@ -2,11 +2,8 @@
|
|||||||
#import "/tail/bibliography.typ": *
|
#import "/tail/bibliography.typ": *
|
||||||
#import "/tail/glossary.typ": *
|
#import "/tail/glossary.typ": *
|
||||||
#import "/main/architecture/description.typ": *
|
#import "/main/architecture/description.typ": *
|
||||||
|
|
||||||
#import "/resources/slides.typ": *
|
#import "/resources/slides.typ": *
|
||||||
|
|
||||||
== Gateway — BLE to MQTT Bridge
|
== Gateway — BLE to MQTT Bridge
|
||||||
|
|
||||||
#slide[
|
#slide[
|
||||||
#grid(
|
#grid(
|
||||||
columns: (1fr, 1fr),
|
columns: (1fr, 1fr),
|
||||||
@@ -22,18 +19,25 @@
|
|||||||
[
|
[
|
||||||
*Data flow*\
|
*Data flow*\
|
||||||
Thingy:52
|
Thingy:52
|
||||||
-> BLE advertising
|
$->$ BLE advertising
|
||||||
Raspberry Pi
|
Raspberry Pi
|
||||||
-> MQTTS (TLS)
|
$->$ MQTTS (TLS)
|
||||||
RabbitMQ broker
|
RabbitMQ broker
|
||||||
->
|
$->$
|
||||||
InfluxDB
|
InfluxDB
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
== Gateway — Overview
|
||||||
|
#slide[
|
||||||
|
#align(center)[
|
||||||
|
#figure(
|
||||||
|
image("/resources/img/gateway_overview.svg", height: 40%),
|
||||||
|
caption: [Gateway communication chain — from @ble:short advertising to database storage]
|
||||||
|
)
|
||||||
|
]
|
||||||
|
]
|
||||||
== Gateway — Key Challenges
|
== Gateway — Key Challenges
|
||||||
|
|
||||||
#slide[
|
#slide[
|
||||||
#grid(
|
#grid(
|
||||||
columns: (1fr, 1fr),
|
columns: (1fr, 1fr),
|
||||||
@@ -48,23 +52,15 @@
|
|||||||
- Deduplication: 10s cache per MAC address
|
- Deduplication: 10s cache per MAC address
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
*Reliability*
|
*Reliability — validated in production*
|
||||||
- MQTT auto-restart via systemd
|
- MQTT auto-restart via systemd
|
||||||
- `os._exit(1)` on disconnection
|
- `os._exit(1)` on disconnection
|
||||||
- Validated with 2 cases:
|
- Validated with 2 cases:
|
||||||
- Network disconnection
|
- Network disconnection
|
||||||
- Wrong MQTT credentials
|
- Wrong MQTT credentials
|
||||||
|
- Multiple nodes publishing
|
||||||
|
simultaneously — confirmed by broker
|
||||||
|
- No duplicate data
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
]
|
|
||||||
|
|
||||||
== Gateway — Data Collection Flow
|
|
||||||
|
|
||||||
#slide[
|
|
||||||
#align(center)[
|
|
||||||
#figure(
|
|
||||||
image("/resources/img/gateway_overview.svg", height: 40%),
|
|
||||||
caption: [Gateway communication chain — from @ble:short advertising to database storage]
|
|
||||||
)
|
|
||||||
]
|
|
||||||
]
|
]
|
||||||
@@ -36,8 +36,10 @@
|
|||||||
caption: [Data communicated in the nodes_interface],
|
caption: [Data communicated in the nodes_interface],
|
||||||
)<tab:nodes_interface_content>
|
)<tab:nodes_interface_content>
|
||||||
|
|
||||||
---
|
|
||||||
== Nodes | Takeaways
|
== Nodes | Takeaways
|
||||||
|
#align(top+left)[
|
||||||
|
#v(5em)
|
||||||
- Breadboard validation #pause
|
- Breadboard validation #pause
|
||||||
- 28 days later #pause
|
- 28 days later #pause
|
||||||
- Improve @ble:short reliability
|
- Improve @ble:short reliability
|
||||||
|
]
|
||||||
BIN
report/meetings/260611-final/server.png
Normal file
BIN
report/meetings/260611-final/server.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 146 KiB |
13
report/meetings/260611-final/server.uxf
Normal file
13
report/meetings/260611-final/server.uxf
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<diagram program="umletino" version="15.1"><zoom_level>10</zoom_level><element><id>UMLDeployment</id><coordinates><x>310</x><y>280</y><w>330</w><h>350</h></coordinates><panel_attributes>Server</panel_attributes><additional_attributes></additional_attributes></element><element><id>UMLGeneric</id><coordinates><x>340</x><y>390</y><w>100</w><h>40</h></coordinates><panel_attributes><<Broker MQTT>>
|
||||||
|
RabbitMQ</panel_attributes><additional_attributes></additional_attributes></element><element><id>UMLGeneric</id><coordinates><x>490</x><y>320</y><w>100</w><h>40</h></coordinates><panel_attributes><<Database>>
|
||||||
|
InfluxDB3</panel_attributes><additional_attributes></additional_attributes></element><element><id>UMLGeneric</id><coordinates><x>490</x><y>390</y><w>100</w><h>40</h></coordinates><panel_attributes><<API DB>>
|
||||||
|
Go service</panel_attributes><additional_attributes></additional_attributes></element><element><id>Relation</id><coordinates><x>430</x><y>400</y><w>80</w><h>30</h></coordinates><panel_attributes>lt=<-</panel_attributes><additional_attributes>60;10;10;10</additional_attributes></element><element><id>Relation</id><coordinates><x>560</x><y>350</y><w>30</w><h>60</h></coordinates><panel_attributes>lt=-></panel_attributes><additional_attributes>10;10;10;40</additional_attributes></element><element><id>Relation</id><coordinates><x>510</x><y>350</y><w>30</w><h>60</h></coordinates><panel_attributes>lt=-></panel_attributes><additional_attributes>10;40;10;10</additional_attributes></element><element><id>Relation</id><coordinates><x>230</x><y>390</y><w>130</w><h>50</h></coordinates><panel_attributes>lt=-()
|
||||||
|
m2=MQTT
|
||||||
|
</panel_attributes><additional_attributes>110;20;10;20</additional_attributes></element><element><id>Relation</id><coordinates><x>530</x><y>430</y><w>190</w><h>50</h></coordinates><panel_attributes>lt=-()
|
||||||
|
m2=REST
|
||||||
|
</panel_attributes><additional_attributes>10;20;170;20</additional_attributes></element><element><id>UMLDeployment</id><coordinates><x>330</x><y>510</y><w>110</w><h>50</h></coordinates><panel_attributes><<Dashboard>>
|
||||||
|
Angular</panel_attributes><additional_attributes></additional_attributes></element><element><id>Relation</id><coordinates><x>430</x><y>490</y><w>140</w><h>60</h></coordinates><panel_attributes>lt=)-
|
||||||
|
m1=
|
||||||
|
</panel_attributes><additional_attributes>110;20;110;40;10;40</additional_attributes></element><element><id>UMLDeployment</id><coordinates><x>330</x><y>570</y><w>110</w><h>50</h></coordinates><panel_attributes><<Notification>>
|
||||||
|
Java</panel_attributes><additional_attributes></additional_attributes></element><element><id>Relation</id><coordinates><x>430</x><y>520</y><w>130</w><h>90</h></coordinates><panel_attributes></panel_attributes><additional_attributes>110;10;110;70;10;70</additional_attributes></element><element><id>Relation</id><coordinates><x>530</x><y>420</y><w>60</w><h>90</h></coordinates><panel_attributes>lt=-()
|
||||||
|
m2=REST</panel_attributes><additional_attributes>10;10;10;70</additional_attributes></element></diagram>
|
||||||
@@ -4,7 +4,6 @@
|
|||||||
#import "/main/architecture/description.typ": *
|
#import "/main/architecture/description.typ": *
|
||||||
|
|
||||||
#import "/resources/slides.typ": *
|
#import "/resources/slides.typ": *
|
||||||
|
|
||||||
#show:make-glossary
|
#show:make-glossary
|
||||||
#register-glossary(entry-list)
|
#register-glossary(entry-list)
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,10 @@
|
|||||||
|
|
||||||
#import "/resources/slides.typ": *
|
#import "/resources/slides.typ": *
|
||||||
|
|
||||||
|
// Chemin racine des images — adapter selon ta structure de projet
|
||||||
|
#let img-root = "../../resources/img/ui_images/images/"
|
||||||
|
|
||||||
|
// ── Palette ───────────────────────────────────────────────────────────────────
|
||||||
#let c-dark = rgb("#0F172A")
|
#let c-dark = rgb("#0F172A")
|
||||||
#let c-teal = rgb("#0EA5E9")
|
#let c-teal = rgb("#0EA5E9")
|
||||||
#let c-text = rgb("#1E293B")
|
#let c-text = rgb("#1E293B")
|
||||||
@@ -12,105 +16,180 @@
|
|||||||
#let c-white = rgb("#FFFFFF")
|
#let c-white = rgb("#FFFFFF")
|
||||||
#let c-border = rgb("#E2E8F0")
|
#let c-border = rgb("#E2E8F0")
|
||||||
|
|
||||||
---
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
─────────────────────────────────────────────────────────────────────────────
|
#let icon-circle(img-path, size: 38pt, bg: rgb("#0EA5E9")) = {
|
||||||
// SLIDE 2 — Cycle DevSecOps
|
box(
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
width: size, height: size,
|
||||||
#let slide-devsecops() = {
|
fill: bg, radius: (size / 2),
|
||||||
// Header custom (subtitle à droite)
|
inset: 0pt, clip: true,
|
||||||
block(width: 100%, height: 43.9pt, fill: c-dark, inset: 0pt)[
|
)[
|
||||||
#pad(x: 25.5pt)[
|
#align(center + horizon)[
|
||||||
#align(horizon)[
|
#image(img-path, width: (size * 0.62), height: (size * 0.62), fit: "contain")
|
||||||
#grid(columns: (1fr, auto),
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
#let badge(label, fill: rgb("#EF4444")) = {
|
||||||
|
box(fill: fill, radius: 3pt, inset: (x: 5pt, y: 2pt))[
|
||||||
|
#text(size: 6.5pt, weight: "bold", fill: rgb("#FFFFFF"))[#label]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
#let devsec-col(phase-title, items) = {
|
||||||
|
block(width: 100%)[
|
||||||
|
#block(
|
||||||
|
width: 100%, height: 28pt,
|
||||||
|
fill: c-dark,
|
||||||
|
radius: (top-left: 5pt, top-right: 5pt, bottom-left: 0pt, bottom-right: 0pt),
|
||||||
|
inset: 0pt,
|
||||||
|
)[
|
||||||
|
#pad(x: 7pt)[
|
||||||
|
#align(horizon + center)[
|
||||||
|
#text(size: 8.5pt, weight: "bold", fill: rgb("#FFFFFF"))[#phase-title]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
#block(
|
||||||
|
width: 100%,
|
||||||
|
fill: rgb("#F1F5F9"),
|
||||||
|
radius: (top-left: 0pt, top-right: 0pt, bottom-left: 5pt, bottom-right: 5pt),
|
||||||
|
inset: 8pt,
|
||||||
|
)[
|
||||||
|
#for item in items {
|
||||||
|
grid(columns: (30pt, 1fr), gutter: 6pt,
|
||||||
|
icon-circle(item.at("icon"), size: 28pt, bg: item.at("bg", default: c-teal)),
|
||||||
align(left + horizon)[
|
align(left + horizon)[
|
||||||
#text(size: 16pt, weight: "bold", fill: rgb("#FFFFFF"))[Cycle DevSecOps - PI E2EEDA]
|
#text(size: 8pt, weight: "bold", fill: c-text)[#item.at("name")]
|
||||||
],
|
#linebreak()
|
||||||
align(right + horizon)[
|
#item.at("extra", default: [])
|
||||||
#text(size: 8pt, fill: rgb("#94A3B8"))[Shift-left · Security by design]
|
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
v(5pt)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
== Cycle DevSecOps
|
||||||
|
|
||||||
|
#slide[
|
||||||
|
#grid(columns: (1fr, 1fr, 1fr, 1fr), gutter: 8pt,
|
||||||
|
devsec-col("① Code & PR Gate", (
|
||||||
|
(icon: img-root + "image10.png", bg: rgb("#3178C6"), name: "TypeScript",
|
||||||
|
extra: [#text(size: 6.5pt, fill: c-muted)[tsc / ESLint]]),
|
||||||
|
(icon: img-root + "image11.png", bg: rgb("#DD0031"), name: "Angular CI",
|
||||||
|
extra: [#text(size: 6.5pt, fill: c-muted)[Build check]]),
|
||||||
|
(icon: img-root + "image19.png", bg: rgb("#1F2328"), name: "GitHub Actions",
|
||||||
|
extra: [#text(size: 6.5pt, fill: c-muted)[Coverage]]),
|
||||||
|
)),
|
||||||
|
devsec-col("② SAST · SCA", (
|
||||||
|
(icon: img-root + "image7.png", bg: rgb("#000000"), name: "SpotBugs",
|
||||||
|
extra: [#badge("BLOCKING")]),
|
||||||
|
(icon: img-root + "image13.png", bg: rgb("#1F2328"), name: "CodeQL",
|
||||||
|
extra: [#badge("BLOCKING")]),
|
||||||
|
(icon: img-root + "image14.png", bg: rgb("#F97316"), name: "Dep. Check",
|
||||||
|
extra: [#badge("NON-BLOCK", fill: rgb("#F97316"))]),
|
||||||
|
)),
|
||||||
|
devsec-col("③ DAST · Tests", (
|
||||||
|
(icon: img-root + "image15.png", bg: rgb("#00549E"), name: "OWASP ZAP",
|
||||||
|
extra: [#badge("BLOCKING")]),
|
||||||
|
(icon: img-root + "image16.png", bg: rgb("#DD0031"), name: "Karma Tests",
|
||||||
|
extra: [#badge("BLOCKING")]),
|
||||||
|
(icon: img-root + "image17.png", bg: rgb("#64748B"), name: "Runtime check",
|
||||||
|
extra: [#text(size: 6.5pt, fill: c-muted)[HTTP headers]]),
|
||||||
|
)),
|
||||||
|
devsec-col("④ Build · Deploy", (
|
||||||
|
(icon: img-root + "image18.png", bg: rgb("#0DB7ED"), name: "Docker",
|
||||||
|
extra: [#text(size: 6.5pt, fill: c-muted)[SHA-tagged]]),
|
||||||
|
(icon: img-root + "image19.png", bg: rgb("#1F2328"), name: "GHCR Push",
|
||||||
|
extra: [#text(size: 6.5pt, fill: c-muted)[main only]]),
|
||||||
|
(icon: img-root + "image17.png", bg: rgb("#10B981"), name: "SSH Deploy",
|
||||||
|
extra: [#text(size: 6.5pt, fill: c-muted)[cert-auth]]),
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
|
||||||
|
#v(7pt)
|
||||||
|
|
||||||
|
#rect(width: 100%, height: 26pt, fill: c-dark, radius: 4pt, inset: 0pt)[
|
||||||
|
#pad(x: 20pt)[
|
||||||
|
#align(horizon)[
|
||||||
|
#grid(columns: (1fr, 1fr, 1fr),
|
||||||
|
align(center + horizon)[#text(size: 8pt, fill: rgb("#FFFFFF"))[🔐 #h(2pt) Shift-left security]],
|
||||||
|
align(center + horizon)[#text(size: 8pt, fill: rgb("#FFFFFF"))[🔑 #h(2pt) Zero secret in code]],
|
||||||
|
align(center + horizon)[#text(size: 8pt, fill: rgb("#FFFFFF"))[🔄 #h(2pt) Automated Deployment]],
|
||||||
|
)
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
||||||
pad(x: 17pt, top: 11pt, bottom: 9pt)[
|
#v(6pt)
|
||||||
#grid(columns: (1fr, 1fr, 1fr, 1fr), gutter: 10pt,
|
]
|
||||||
|
|
||||||
// ① Code & PR Gate
|
|
||||||
// image10 = TypeScript (TS logo), image11 = Angular (grisé)
|
|
||||||
// image19 = GitHub Actions (GHCR/GitHub logo)
|
|
||||||
devsec-col("① Code & PR Gate", (
|
|
||||||
(icon: "../../resources/img/ui_images/images/image10.png", bg: rgb("#3178C6"), name: "TypeScript",
|
|
||||||
extra: [#text(size: 7pt, fill: c-muted)[tsc / ESLint]]),
|
|
||||||
(icon: "../../resources/img/ui_images/images/image11.png", bg: rgb("#DD0031"), name: "Angular CI",
|
|
||||||
extra: [#text(size: 7pt, fill: c-muted)[Build check]]),
|
|
||||||
)),
|
|
||||||
|
|
||||||
// ② SAST · SCA
|
// ── SLIDE 3 — Dashboard ───────────────────────────────────────────────────────
|
||||||
// image13 = GitHub octocat (SpotBugs hébergé GitHub), image13 aussi CodeQL (GitHub)
|
== Dashboard
|
||||||
// image14 = checklist hexagone (Dep. Check / OWASP DC)
|
|
||||||
devsec-col("② SAST · SCA", (
|
|
||||||
(icon: "../../resources/img/ui_images/images/image13.png", bg: rgb("#1F2328"), name: "SpotBugs",
|
|
||||||
extra: [#badge("BLOCKING")]),
|
|
||||||
(icon: "../../resources/img/ui_images/images/image13.png", bg: rgb("#1F2328"), name: "CodeQL",
|
|
||||||
extra: [#badge("BLOCKING")]),
|
|
||||||
(icon: "../../resources/img/ui_images/images/image14.png", bg: rgb("#F97316"), name: "Dep. Check",
|
|
||||||
extra: [#badge("NON-BLOCK", fill: rgb("#F97316"))]),
|
|
||||||
)),
|
|
||||||
|
|
||||||
// ③ DAST · Tests
|
#slide[
|
||||||
// image15 = libellule OWASP ZAP, image16 = Karma/Jasmine, image17 = Tux Linux
|
#align(center + horizon)[
|
||||||
devsec-col("③ DAST · Tests", (
|
#figure(
|
||||||
(icon: "../../resources/img/ui_images/images/image15.png", bg: rgb("#00549E"), name: "OWASP ZAP",
|
image(img-root + "image20.png", width: 100%, fit: "contain"),
|
||||||
extra: [#badge("BLOCKING")]),
|
caption: [Dashboard]
|
||||||
(icon: "../../resources/img/ui_images/images/image16.png", bg: rgb("#DD0031"), name: "Karma Tests",
|
)
|
||||||
extra: [#badge("BLOCKING")]),
|
|
||||||
(icon: "../../resources/img/ui_images/images/image17.png", bg: rgb("#64748B"), name: "Runtime check",
|
|
||||||
extra: [#text(size: 7pt, fill: c-muted)[HTTP headers]]),
|
|
||||||
)),
|
|
||||||
|
|
||||||
// ④ Build · Deploy
|
|
||||||
// image18 = Docker, image19 = GHCR, image17 = Tux Linux (SSH)
|
|
||||||
devsec-col("④ Build · Deploy", (
|
|
||||||
(icon: "../../resources/img/ui_images/images/image18.png", bg: rgb("#0DB7ED"), name: "Docker",
|
|
||||||
extra: [#text(size: 7pt, fill: c-muted)[SHA-tagged]]),
|
|
||||||
(icon: "../../resources/img/ui_images/images/image19.png", bg: rgb("#1F2328"), name: "GHCR Push",
|
|
||||||
extra: [#text(size: 7pt, fill: c-muted)[main only]]),
|
|
||||||
(icon: "../../resources/img/ui_images/images/image17.png", bg: rgb("#10B981"), name: "SSH Deploy",
|
|
||||||
extra: [#text(size: 7pt, fill: c-muted)[cert-auth]]),
|
|
||||||
)),
|
|
||||||
)
|
|
||||||
|
|
||||||
#v(9pt)
|
|
||||||
|
|
||||||
// Barre résumé
|
|
||||||
#rect(width: 100%, height: 30pt, fill: c-dark, radius: 5pt, inset: 0pt)[
|
|
||||||
#pad(x: 30pt)[
|
|
||||||
#align(horizon)[
|
|
||||||
#grid(columns: (1fr, 1fr, 1fr),
|
|
||||||
align(center + horizon)[#text(size: 9pt, fill: rgb("#FFFFFF"))[🔐 #h(3pt) Shift-left security]],
|
|
||||||
align(center + horizon)[#text(size: 9pt, fill: rgb("#FFFFFF"))[🔑 #h(3pt) Zero secret in code]],
|
|
||||||
align(center + horizon)[#text(size: 9pt, fill: rgb("#FFFFFF"))[🔄 #h(3pt) Automated Deployment]],
|
|
||||||
)
|
|
||||||
]
|
|
||||||
]
|
|
||||||
]
|
|
||||||
|
|
||||||
#v(8pt)
|
|
||||||
|
|
||||||
// GitHub Actions — image19 (GitHub/GHCR logo)
|
|
||||||
#align(center)[
|
|
||||||
#grid(columns: (auto, auto, auto), gutter: 7pt,
|
|
||||||
align(horizon)[#icon-circle("../../resources/img/ui_images/images/image19.png", size: 24pt, bg: rgb("#1F2328"))],
|
|
||||||
align(horizon)[#text(size: 8.5pt, weight: "bold", fill: c-text)[GitHub Actions]],
|
|
||||||
align(horizon)[#text(size: 8.5pt, fill: c-muted)[Coverage]],
|
|
||||||
)
|
|
||||||
]
|
|
||||||
]
|
]
|
||||||
|
]
|
||||||
|
|
||||||
v(1fr)
|
// ── SLIDE 4 — Details Page ────────────────────────────────────────────────────
|
||||||
|
== Details Page
|
||||||
}
|
|
||||||
|
|
||||||
//
|
#slide[
|
||||||
|
#align(center + horizon)[
|
||||||
|
#figure(
|
||||||
|
image(img-root + "image21.png", width: 100%, fit: "contain"),
|
||||||
|
caption: [Details page]
|
||||||
|
)
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
// ── SLIDE 5 — Notification ────────────────────────────────────────────────────
|
||||||
|
== Notification
|
||||||
|
|
||||||
|
#slide[
|
||||||
|
#grid(columns: (1fr, 1fr), gutter: 16pt,
|
||||||
|
// Screenshot Telegram
|
||||||
|
align(center + horizon)[
|
||||||
|
|
||||||
|
#figure(
|
||||||
|
image(img-root + "image22.png", height: 300pt, fit: "contain"),
|
||||||
|
caption: [Telegram notification]
|
||||||
|
)
|
||||||
|
],
|
||||||
|
// Carte descriptive
|
||||||
|
rect(width: 100%, radius: 6pt, stroke: 0.5pt + c-border, fill: rgb("#FFFFFF"), inset: 12pt)[
|
||||||
|
#text(size: 10pt, weight: "bold", fill: c-text)[CO₂ Alerts System]
|
||||||
|
#v(5pt)
|
||||||
|
#line(length: 100%, stroke: 0.5pt + c-border)
|
||||||
|
#v(6pt)
|
||||||
|
|
||||||
|
#let alert-row(col, level, desc) = {
|
||||||
|
grid(columns: (10pt, 52pt, 1fr), gutter: 5pt,
|
||||||
|
box(width: 8pt, height: 8pt, fill: col, radius: 4pt),
|
||||||
|
text(size: 8.5pt, weight: "bold")[#level],
|
||||||
|
text(size: 8pt, fill: c-muted)[#desc],
|
||||||
|
)
|
||||||
|
v(4pt)
|
||||||
|
}
|
||||||
|
#alert-row(rgb("#EF4444"), "Critical", "> 2000 ppm")
|
||||||
|
#alert-row(rgb("#F97316"), "Poor", "1200–1500 ppm")
|
||||||
|
#alert-row(rgb("#EAB308"), "Moderate", "1000–1200 ppm")
|
||||||
|
#alert-row(rgb("#22C55E"), "Good", "< 800 ppm")
|
||||||
|
|
||||||
|
#v(6pt)
|
||||||
|
#line(length: 100%, stroke: 0.5pt + c-border)
|
||||||
|
#v(6pt)
|
||||||
|
|
||||||
|
#v(6pt)
|
||||||
|
|
||||||
|
],
|
||||||
|
)
|
||||||
|
]
|
||||||
Reference in New Issue
Block a user