feat: enhance configuration and templates for HTML output

This commit is contained in:
2026-03-25 12:51:45 +09:00
parent c7ea631ec2
commit 91f355c17e
7 changed files with 155 additions and 90 deletions
+7 -6
View File
@@ -1,7 +1,8 @@
{
"recommendations": [
"1yib.rust-bundle",
"streetsidesoftware.code-spell-checker",
"esbenp.prettier-vscode"
]
}
"recommendations": [
"1yib.rust-bundle",
"streetsidesoftware.code-spell-checker",
"esbenp.prettier-vscode",
"karunamurti.tera"
]
}
+5 -4
View File
@@ -73,14 +73,14 @@ Pass `--verbose` if you want the CLI to print the selected configuration file pa
Configure at least these values:
- `digitemp` and `digitemp_config`
- `database_dir` and `images_dir`
- `templates_dir`
- `database_dir`, `html_dir`, and `images_dir`
- `templates_dir` and `html_template`
- `room_name`
- SMTP server settings
- mail recipients, subjects, and alert thresholds
`room_name` is required. It is used in mail templates, graph titles, and the generated top page.
The `update` command also regenerates `html/index.html` from the template so the page title and heading stay aligned with `room_name`.
The `update` command renders `html_template` and writes it under `html_dir` using the same relative path with the `.tera` suffix removed. For example, `index.html.tera` becomes `html/index.html`.
SMTP settings support these combinations:
@@ -140,7 +140,8 @@ See `etc/crontab.sample` for a sample schedule.
## Templates
Mail templates are stored in `templates/` and rendered with [Tera](https://tera.netlify.app/).
Templates are stored in `templates/` and rendered with [Tera](https://tera.netlify.app/).
The top page uses `html_template`, while the mail bodies use `mail_info_template` and `mail_alert_template`.
## Development Notes
+54 -3
View File
@@ -15,14 +15,21 @@ fn default_mail_smtp_auth() -> bool {
true
}
fn default_html_template() -> String {
"index.html.tera".to_string()
}
#[derive(Debug, Deserialize)]
pub struct Config {
pub url: String,
pub digitemp: String,
pub digitemp_config: String,
pub database_dir: String,
pub html_dir: String,
pub images_dir: String,
pub templates_dir: String,
#[serde(default = "default_html_template")]
pub html_template: String,
pub room_name: String,
pub mail_alert_upper: f64,
pub mail_alert_lower: f64,
@@ -57,6 +64,46 @@ mod tests {
"digitemp": "/bin/digitemp_DS9097U",
"digitemp_config": "etc/digitemp.conf",
"database_dir": "db",
"html_dir": "html",
"images_dir": "html/images",
"templates_dir": "templates",
"html_template": "index.html.tera",
"room_name": "S108",
"mail_alert_upper": 30,
"mail_alert_lower": 17,
"mail_from": "from@example.com",
"mail_from_name": "Temperature Monitor",
"mail_info_to": "info@example.com",
"mail_info_subject": "Info",
"mail_info_template": "mail_info.tera",
"mail_alert_to": "alert@example.com",
"mail_alert_subject": "Alert",
"mail_alert_template": "mail_alert.tera",
"mail_smtp_server": "localhost",
"mail_smtp_port": 587,
"mail_smtp_username": "user@example.com",
"mail_smtp_password": "password"
}"#;
let config: Config = serde_json::from_str(json).unwrap();
assert!(matches!(
config.mail_smtp_security,
MailSmtpSecurity::Starttls
));
assert!(config.mail_smtp_auth);
assert_eq!(config.html_template, "index.html.tera");
assert_eq!(config.room_name().unwrap(), "S108");
}
#[test]
fn config_defaults_html_template() {
let json = r#"{
"url": "https://example.com/temperature/",
"digitemp": "/bin/digitemp_DS9097U",
"digitemp_config": "etc/digitemp.conf",
"database_dir": "db",
"html_dir": "html",
"images_dir": "html/images",
"templates_dir": "templates",
"room_name": "S108",
@@ -78,9 +125,7 @@ mod tests {
let config: Config = serde_json::from_str(json).unwrap();
assert!(matches!(config.mail_smtp_security, MailSmtpSecurity::Starttls));
assert!(config.mail_smtp_auth);
assert_eq!(config.room_name().unwrap(), "S108");
assert_eq!(config.html_template, "index.html.tera");
}
#[test]
@@ -90,8 +135,10 @@ mod tests {
"digitemp": "/bin/digitemp_DS9097U",
"digitemp_config": "etc/digitemp.conf",
"database_dir": "db",
"html_dir": "html",
"images_dir": "html/images",
"templates_dir": "templates",
"html_template": "index.html.tera",
"room_name": "S108",
"mail_alert_upper": 30,
"mail_alert_lower": 17,
@@ -125,8 +172,10 @@ mod tests {
"digitemp": "/bin/digitemp_DS9097U",
"digitemp_config": "etc/digitemp.conf",
"database_dir": "db",
"html_dir": "html",
"images_dir": "html/images",
"templates_dir": "templates",
"html_template": "index.html.tera",
"room_name": " ",
"mail_alert_upper": 30,
"mail_alert_lower": 17,
@@ -156,8 +205,10 @@ mod tests {
"digitemp": "/bin/digitemp_DS9097U",
"digitemp_config": "etc/digitemp.conf",
"database_dir": "db",
"html_dir": "html",
"images_dir": "html/images",
"templates_dir": "templates",
"html_template": "index.html.tera",
"mail_alert_upper": 30,
"mail_alert_lower": 17,
"mail_from": "from@example.com",
+2
View File
@@ -3,8 +3,10 @@
"digitemp": "/bin/digitemp_DS9097U",
"digitemp_config": "/opt/digitemp-rrdgraph/etc/digitemp.conf",
"database_dir": "/opt/digitemp-rrdgraph/db",
"html_dir": "/opt/digitemp-rrdgraph/html",
"images_dir": "/opt/digitemp-rrdgraph/html/images",
"templates_dir": "/opt/digitemp-rrdgraph/templates",
"html_template": "index.html.tera",
"room_name": "C407",
"mail_alert_upper": 30,
"mail_alert_lower": 17,
+13 -5
View File
@@ -7,6 +7,14 @@ use std::fs;
use std::path::Path;
use std::time::{SystemTime, UNIX_EPOCH};
fn output_path_for_template(config: &Config) -> Result<std::path::PathBuf, Box<dyn Error>> {
let output_name = config
.html_template
.strip_suffix(".tera")
.ok_or("html_template must end with .tera")?;
Ok(Path::new(&config.html_dir).join(output_name))
}
pub fn run(config: &Config) -> Result<(), Box<dyn Error>> {
let room_name = config.room_name()?;
let digitemp = DigiTemp::new(config.digitemp.clone(), config.digitemp_config.clone())?;
@@ -41,11 +49,11 @@ pub fn run(config: &Config) -> Result<(), Box<dyn Error>> {
let engine = TemplateEngine::new(&config.templates_dir)?;
let mut ctx = TemplateContext::new();
ctx.insert("room_name", &room_name);
let html = engine.render("index.tera", &ctx)?;
let index_path = Path::new(&config.images_dir)
.parent()
.unwrap_or_else(|| Path::new("html"))
.join("index.html");
let html = engine.render(&config.html_template, &ctx)?;
let index_path = output_path_for_template(config)?;
if let Some(parent) = index_path.parent() {
fs::create_dir_all(parent)?;
}
fs::write(index_path, html)?;
Ok(())
+74
View File
@@ -0,0 +1,74 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Temperature Monitor - {{ room_name }}</title>
<meta name="description" content="Temperature Monitor - {{ room_name }}">
<meta name="author" content="Neuroinformatics Unit, RIKEN CBS">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta http-equiv="refresh" content="300">
<link rel="stylesheet" href="css/default.css">
</head>
<body>
<h1>Temperature Monitor - {{ room_name }}</h1>
<main>
<div class="graph">
<div class="range-select">
Hour:
<span onclick="changeGraph('g1', 'hour', 1)">1</span> ...
<span onclick="changeGraph('g1', 'hour', 3)">3</span> ...
<span onclick="changeGraph('g1', 'hour', 6)">6</span> ...
<span onclick="changeGraph('g1', 'hour', 12)">12</span>
</div>
<div><img id="g1" src="./images/graph-hour.png" alt="Hourly Graph" /></div>
</div>
<div class="graph">
<div class="range-select">
Day:
<span onclick="changeGraph('g2', 'day', 1)">1</span> ...
<span onclick="changeGraph('g2', 'day', 3)">3</span>
</div>
<div><img id="g2" src="./images/graph-day.png" alt="Daily Graph" /></div>
</div>
<div class="graph">
<div class="range-select">
Week:
<span onclick="changeGraph('g3', 'week', 1)">1</span> ...
<span onclick="changeGraph('g3', 'week', 2)">2</span>
</div>
<div><img id="g3" src="./images/graph-week.png" alt="Weekly Graph" /></div>
</div>
<div class="graph">
<div class="range-select">
Month:
<span onclick="changeGraph('g4', 'month', 1)">1</span> ...
<span onclick="changeGraph('g4', 'month', 3)">3</span> ...
<span onclick="changeGraph('g4', 'month', 6)">6</span>
</div>
<div><img id="g4" src="./images/graph-month.png" alt="Monthly Graph" /></div>
</div>
<div class="graph">
<div class="range-select">
Year:
<span onclick="changeGraph('g5', 'year', 1)">1</span> ...
<span onclick="changeGraph('g5', 'year', 3)">3</span> ...
<span onclick="changeGraph('g5', 'year', 5)">5</span> ...
<span onclick="changeGraph('g5', 'year', 10)">10</span>
</div>
<div><img id="g5" src="./images/graph-year.png" alt="Yearly Graph" /></div>
</div>
</main>
<script>
function changeGraph(id, type, num) {
const img = document.getElementById(id);
if (img) {
img.src = './images/graph-' + (num === 1 ? '' : num) + type + '.png';
}
}
</script>
</body>
</html>
-72
View File
@@ -1,72 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Temperature Monitor - {{ room_name }}</title>
<meta name="description" content="Temperature Monitor - {{ room_name }}">
<meta name="author" content="Neuroinformatics Unit, RIKEN CBS">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta http-equiv="refresh" content="300">
<link rel="stylesheet" href="css/default.css">
</head>
<body>
<h1>Temperature Monitor - {{ room_name }}</h1>
<main>
<div class="graph">
<div class="range-select">
Hour:
<span onclick="changeGraph('g1', 'hour', 1)">1</span> ...
<span onclick="changeGraph('g1', 'hour', 3)">3</span> ...
<span onclick="changeGraph('g1', 'hour', 6)">6</span> ...
<span onclick="changeGraph('g1', 'hour', 12)">12</span>
</div>
<div><img id="g1" src="./images/graph-hour.png" alt="Hourly Graph" /></div>
</div>
<div class="graph">
<div class="range-select">
Day:
<span onclick="changeGraph('g2', 'day', 1)">1</span> ...
<span onclick="changeGraph('g2', 'day', 3)">3</span>
</div>
<div><img id="g2" src="./images/graph-day.png" alt="Daily Graph" /></div>
</div>
<div class="graph">
<div class="range-select">
Week:
<span onclick="changeGraph('g3', 'week', 1)">1</span> ...
<span onclick="changeGraph('g3', 'week', 2)">2</span>
</div>
<div><img id="g3" src="./images/graph-week.png" alt="Weekly Graph" /></div>
</div>
<div class="graph">
<div class="range-select">
Month:
<span onclick="changeGraph('g4', 'month', 1)">1</span> ...
<span onclick="changeGraph('g4', 'month', 3)">3</span> ...
<span onclick="changeGraph('g4', 'month', 6)">6</span>
</div>
<div><img id="g4" src="./images/graph-month.png" alt="Monthly Graph" /></div>
</div>
<div class="graph">
<div class="range-select">
Year:
<span onclick="changeGraph('g5', 'year', 1)">1</span> ...
<span onclick="changeGraph('g5', 'year', 3)">3</span> ...
<span onclick="changeGraph('g5', 'year', 5)">5</span> ...
<span onclick="changeGraph('g5', 'year', 10)">10</span>
</div>
<div><img id="g5" src="./images/graph-year.png" alt="Yearly Graph" /></div>
</div>
</main>
<script>
function changeGraph(id, type, num) {
const img = document.getElementById(id);
if (img) {
img.src = './images/graph-' + (num === 1 ? '' : num) + type + '.png';
}
}
</script>
</body>
</html>