Skip to main content

ryra_core/config/
status.rs

1use std::path::PathBuf;
2
3use super::schema::*;
4use crate::metadata::load_metadata;
5
6/// High-level status of the ryra installation.
7pub enum RyraStatus {
8    NotInitialized,
9    Initialized(StatusInfo),
10    Error(String),
11}
12
13pub struct StatusInfo {
14    pub config_path: PathBuf,
15    pub smtp: ProviderStatus,
16    pub auth: ProviderStatus,
17    /// Backup repo configuration + count of services with
18    /// `backup_enabled = true` in their metadata. Absent when no
19    /// `[backup]` section exists in preferences.toml.
20    pub backup: Option<BackupSummary>,
21    /// Tailscale identity + count of services exposed via a tailscale
22    /// URL. Absent when no `[tailscale]` section exists. The presence
23    /// of a tailscale config alone is enough to surface the line;
24    /// `advertised` may be zero if the user pasted a token but hasn't
25    /// `ryra add --tailscale`'d anything yet.
26    pub tailscale: Option<TailscaleSummary>,
27    pub services: Vec<ServiceInfo>,
28}
29
30pub enum ProviderStatus {
31    None,
32    Configured { name: String },
33}
34
35pub struct BackupSummary {
36    /// Short label for the backend (`"S3 (http://127.0.0.1:9000)"`,
37    /// `"local (/var/backups/ryra)"`). Designed to fit on one line.
38    pub backend_label: String,
39    /// Number of installed services with `backup_enabled = true`.
40    pub included: usize,
41}
42
43pub struct TailscaleSummary {
44    /// Number of installed services with a `.ts.net` exposure.
45    pub advertised: usize,
46}
47
48pub struct ServiceInfo {
49    pub name: String,
50    pub url: Option<String>,
51    pub ports: std::collections::BTreeMap<String, u16>,
52    pub installed: bool,
53    /// True when the service's `metadata.toml` records
54    /// `backup_enabled = true`. Used by `ryra status` to count
55    /// services included in the next backup run.
56    pub backup_enabled: bool,
57    /// True when the service's URL classifies as a Tailscale
58    /// exposure (`.ts.net` hostname). Used to count services
59    /// advertised on the tailnet.
60    pub tailscale_exposed: bool,
61}
62
63impl StatusInfo {
64    pub fn from_config(config_path: PathBuf, config: &Config) -> Self {
65        let services: Vec<ServiceInfo> = crate::list_installed()
66            .unwrap_or_default()
67            .into_iter()
68            .map(|s| {
69                // Per-install backup_enabled lives in metadata.toml, not
70                // in the installed-service summary. Load it best-effort
71                // — a missing metadata.toml just reads as "not enrolled."
72                let backup_enabled = load_metadata(&s.name)
73                    .ok()
74                    .flatten()
75                    .map(|m| m.backup_enabled)
76                    .unwrap_or(false);
77                let url = s.exposure.url().map(|u| u.to_string());
78                let tailscale_exposed = url.as_deref().is_some_and(crate::is_tailscale_url);
79                ServiceInfo {
80                    name: s.name,
81                    url,
82                    ports: s.ports,
83                    installed: s.installed,
84                    backup_enabled,
85                    tailscale_exposed,
86                }
87            })
88            .collect();
89
90        let backup = config.backup.as_ref().map(|b| BackupSummary {
91            backend_label: format_backend(&b.backend),
92            included: services.iter().filter(|s| s.backup_enabled).count(),
93        });
94
95        let tailscale = config.tailscale.as_ref().map(|_| TailscaleSummary {
96            advertised: services.iter().filter(|s| s.tailscale_exposed).count(),
97        });
98
99        Self {
100            config_path,
101            smtp: match &config.smtp {
102                None => ProviderStatus::None,
103                Some(smtp) => ProviderStatus::Configured {
104                    name: smtp.host.clone(),
105                },
106            },
107            auth: match &config.auth {
108                None => ProviderStatus::None,
109                Some(auth) => ProviderStatus::Configured {
110                    name: format!("{} ({})", auth.provider_name(), auth.url()),
111                },
112            },
113            backup,
114            tailscale,
115            services,
116        }
117    }
118}
119
120/// One-line label for a backup backend. Strips credentials, keeps the
121/// shape ("S3 at http://…", "local /path") so the user can recognise
122/// where their snapshots go without exposing the secret key.
123fn format_backend(backend: &BackupBackend) -> String {
124    match backend {
125        BackupBackend::S3 {
126            endpoint, bucket, ..
127        } => format!("S3 ({endpoint}/{bucket})"),
128        BackupBackend::Local { path } => format!("local ({})", path.display()),
129        BackupBackend::Managed => "ryra-managed".to_string(),
130    }
131}