Add \`sysop motd\` — opt-out login tip rotation
Four verbs: \`sysop motd\` (status), \`sysop motd on\`, \`sysop motd off\`,
\`sysop motd print\` (called by /etc/profile.d/sysop-motd.sh on login).
Settings live in /etc/mountaineer/sysop.toml (\$SYSOP_CONFIG env override
for host dev). New \`config\` module owns load/save; first piece of sysop
state that will grow as more subcommands need persistent settings.
Tip rotation is time-bucketed (epoch_seconds / 60) — no persistent
counter, no read/write race on shared state, naturally approximates
per-login rotation. Six tips, drawn verbatim from AESTHETICS.md.
Default enabled = true (opt-out, per AESTHETICS).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 files changed,
+118 insertions,
-7 deletions
|
1 |
+ |
use anyhow::{Context, Result};
|
|
2 |
+ |
use serde::{Deserialize, Serialize};
|
|
3 |
+ |
use std::fs;
|
|
4 |
+ |
use std::path::PathBuf;
|
|
5 |
+ |
|
|
6 |
+ |
const DEFAULT_PATH: &str = "/etc/mountaineer/sysop.toml";
|
|
7 |
+ |
|
|
8 |
+ |
pub fn path() -> PathBuf {
|
|
9 |
+ |
std::env::var_os("SYSOP_CONFIG")
|
|
10 |
+ |
.map(PathBuf::from)
|
|
11 |
+ |
.unwrap_or_else(|| PathBuf::from(DEFAULT_PATH))
|
|
12 |
+ |
}
|
|
13 |
+ |
|
|
14 |
+ |
#[derive(Debug, Default, Serialize, Deserialize)]
|
|
15 |
+ |
pub struct Config {
|
|
16 |
+ |
#[serde(default)]
|
|
17 |
+ |
pub motd: Motd,
|
|
18 |
+ |
}
|
|
19 |
+ |
|
|
20 |
+ |
#[derive(Debug, Serialize, Deserialize)]
|
|
21 |
+ |
pub struct Motd {
|
|
22 |
+ |
pub enabled: bool,
|
|
23 |
+ |
}
|
|
24 |
+ |
|
|
25 |
+ |
impl Default for Motd {
|
|
26 |
+ |
fn default() -> Self {
|
|
27 |
+ |
Self { enabled: true }
|
|
28 |
+ |
}
|
|
29 |
+ |
}
|
|
30 |
+ |
|
|
31 |
+ |
pub fn load() -> Result<Config> {
|
|
32 |
+ |
let p = path();
|
|
33 |
+ |
if !p.exists() {
|
|
34 |
+ |
return Ok(Config::default());
|
|
35 |
+ |
}
|
|
36 |
+ |
let s = fs::read_to_string(&p).with_context(|| format!("read {}", p.display()))?;
|
|
37 |
+ |
toml::from_str(&s).with_context(|| format!("parse {}", p.display()))
|
|
38 |
+ |
}
|
|
39 |
+ |
|
|
40 |
+ |
pub fn save(cfg: &Config) -> Result<()> {
|
|
41 |
+ |
let p = path();
|
|
42 |
+ |
if let Some(dir) = p.parent() {
|
|
43 |
+ |
fs::create_dir_all(dir).with_context(|| format!("create {}", dir.display()))?;
|
|
44 |
+ |
}
|
|
45 |
+ |
let s = toml::to_string_pretty(cfg).context("serialize config")?;
|
|
46 |
+ |
fs::write(&p, s).with_context(|| format!("write {}", p.display()))
|
|
47 |
+ |
}
|
|
1 |
+ |
mod config;
|
| 1 |
2 |
|
mod info;
|
|
3 |
+ |
mod motd;
|
| 2 |
4 |
|
mod roles;
|
| 3 |
5 |
|
|
| 4 |
6 |
|
use clap::{Parser, Subcommand};
|
| 17 |
19 |
|
/// Role name (omit for the full list)
|
| 18 |
20 |
|
role: Option<String>,
|
| 19 |
21 |
|
},
|
|
22 |
+ |
/// Login tip rotation (on/off/print)
|
|
23 |
+ |
Motd {
|
|
24 |
+ |
#[command(subcommand)]
|
|
25 |
+ |
action: Option<MotdAction>,
|
|
26 |
+ |
},
|
|
27 |
+ |
}
|
|
28 |
+ |
|
|
29 |
+ |
#[derive(Subcommand)]
|
|
30 |
+ |
enum MotdAction {
|
|
31 |
+ |
/// Enable the login tip
|
|
32 |
+ |
On,
|
|
33 |
+ |
/// Disable the login tip
|
|
34 |
+ |
Off,
|
|
35 |
+ |
/// Emit the current tip (called by /etc/profile.d/sysop-motd.sh on login)
|
|
36 |
+ |
Print,
|
| 20 |
37 |
|
}
|
| 21 |
38 |
|
|
| 22 |
39 |
|
fn main() -> anyhow::Result<()> {
|
| 23 |
40 |
|
let cli = Cli::parse();
|
| 24 |
41 |
|
let code = match cli.command {
|
| 25 |
42 |
|
Command::Info { role } => info::run(role)?,
|
|
43 |
+ |
Command::Motd { action } => match action {
|
|
44 |
+ |
None => motd::status()?,
|
|
45 |
+ |
Some(MotdAction::On) => motd::set(true)?,
|
|
46 |
+ |
Some(MotdAction::Off) => motd::set(false)?,
|
|
47 |
+ |
Some(MotdAction::Print) => motd::print()?,
|
|
48 |
+ |
},
|
| 26 |
49 |
|
};
|
| 27 |
50 |
|
std::process::exit(code);
|
| 28 |
51 |
|
}
|
|
1 |
+ |
use crate::config;
|
|
2 |
+ |
use anyhow::Result;
|
|
3 |
+ |
use std::time::SystemTime;
|
|
4 |
+ |
|
|
5 |
+ |
const TIPS: &[&str] = &[
|
|
6 |
+ |
"sysop info <role> — show which tool fills a role and where its config lives",
|
|
7 |
+ |
"sysop services — s6-rc service health",
|
|
8 |
+ |
"sysop wifi — network connections",
|
|
9 |
+ |
"sysop storage — ZFS pool state",
|
|
10 |
+ |
"sysop logs <svc> — structured logs for a service",
|
|
11 |
+ |
"sysop help — list all sysop commands",
|
|
12 |
+ |
];
|
|
13 |
+ |
|
|
14 |
+ |
pub fn status() -> Result<i32> {
|
|
15 |
+ |
let cfg = config::load()?;
|
|
16 |
+ |
let state = if cfg.motd.enabled { "on" } else { "off" };
|
|
17 |
+ |
println!("motd: {state}");
|
|
18 |
+ |
println!("config: {}", config::path().display());
|
|
19 |
+ |
Ok(0)
|
|
20 |
+ |
}
|
|
21 |
+ |
|
|
22 |
+ |
pub fn set(enabled: bool) -> Result<i32> {
|
|
23 |
+ |
let mut cfg = config::load()?;
|
|
24 |
+ |
cfg.motd.enabled = enabled;
|
|
25 |
+ |
config::save(&cfg)?;
|
|
26 |
+ |
println!("motd: {}", if enabled { "on" } else { "off" });
|
|
27 |
+ |
Ok(0)
|
|
28 |
+ |
}
|
|
29 |
+ |
|
|
30 |
+ |
pub fn print() -> Result<i32> {
|
|
31 |
+ |
let cfg = config::load()?;
|
|
32 |
+ |
if !cfg.motd.enabled {
|
|
33 |
+ |
return Ok(0);
|
|
34 |
+ |
}
|
|
35 |
+ |
let bucket = SystemTime::now()
|
|
36 |
+ |
.duration_since(SystemTime::UNIX_EPOCH)
|
|
37 |
+ |
.map(|d| d.as_secs() / 60)
|
|
38 |
+ |
.unwrap_or(0) as usize;
|
|
39 |
+ |
let idx = bucket % TIPS.len();
|
|
40 |
+ |
println!();
|
|
41 |
+ |
println!(" {}", TIPS[idx]);
|
|
42 |
+ |
Ok(0)
|
|
43 |
+ |
}
|
| 1 |
1 |
|
# mountaineer-sysop — todo
|
| 2 |
2 |
|
|
| 3 |
|
- |
Status: Pre-v0. Active: `sysop motd on/off`. `sysop info` lands end-to-end (list + detail TUI, `cargo run -p sysop -- info` on host). Primitives exercised by `cargo run --example smoketest -p sysop-tui`.
|
|
3 |
+ |
Status: Pre-v0. Active: next subcommand TBD (suggest `sysop services` to unblock the yambar `svc` segment). `sysop info` and `sysop motd` land end-to-end on host. Primitives exercised by `cargo run --example smoketest -p sysop-tui`.
|
| 4 |
4 |
|
|
| 5 |
5 |
|
## Phase 1 — sysop-tui primitives (open items)
|
| 6 |
6 |
|
|
| 14 |
14 |
|
- [ ] Runtime override: layer `/etc/mountaineer/roles.toml` on top of the compiled-in defaults (principle 2 — operator can see/edit). Currently only `include_str!` source
|
| 15 |
15 |
|
- [ ] Server vs Sysadmin profile filter — graphical-layer roles should be hidden in `--server` mode
|
| 16 |
16 |
|
|
| 17 |
|
- |
## Phase 3 — `sysop motd on/off`
|
|
17 |
+ |
## Phase 3 — `sysop motd` (open items)
|
| 18 |
18 |
|
|
| 19 |
|
- |
Smallest stateful subcommand. Writes the motd-enable flag (from installer or operator). Exercises config-write path.
|
| 20 |
|
- |
|
| 21 |
|
- |
- [ ] Config file location decided (`/etc/mountaineer/sysop.toml`? `~/.config/mountaineer/`? both?)
|
| 22 |
|
- |
- [ ] `sysop motd on` / `sysop motd off` / `sysop motd` (status)
|
| 23 |
|
- |
- [ ] motd rotation script driven by this flag
|
|
19 |
+ |
- [ ] `/etc/profile.d/sysop-motd.sh` glue script — calls `sysop motd print` on login. Lives in **mountaineer-build apkovl-src**, not this repo
|
|
20 |
+ |
- [ ] Tip set worth revisiting once more `sysop` verbs exist — currently 6, drawn from AESTHETICS
|
|
21 |
+ |
- [ ] Decide whether `sysop motd print` should respect a "first-login-of-the-day" suppression to avoid tip spam in tmux-heavy sessions
|
| 24 |
22 |
|
|
| 25 |
23 |
|
## Deferred (post-MVP scaffolding)
|
| 26 |
24 |
|
|