Skip to main content

pamoja_dashboard/
lib.rs

1//! Local-first dashboard for a pamoja node.
2//!
3//! A node serves its own dashboard over its own WiFi hotspot, so a clinic worker, a
4//! farmer, or a water committee sees their own data with no internet at all, in their
5//! own language, on whatever cheap phone they have. This crate is the host side of
6//! that: it turns the state a node already holds into a small, language-neutral
7//! snapshot and serves a hand-built, localized page that renders it.
8//!
9//! The design rests on one split. The device emits only a [`State`] - stable keys,
10//! stable codes, raw values, and canonical units, identical in every locale - and the
11//! page does all rendering, formatting, and translation at the surface. That keeps the
12//! device's job tiny enough for constrained hardware and the page's job rich enough to
13//! be beautiful, and it means localization is a property of the page, not a fork of
14//! the data.
15//!
16//! The pieces:
17//!
18//! - [`State`] is the language-neutral fleet snapshot served at `GET /state`:
19//!   [`Org`]s of [`Group`]s of [`Sensor`]s, each group on its own [`Link`].
20//! - [`StateSource`] is the one seam between the dashboard and its data; a real
21//!   gateway and the `Mock` both implement it.
22//! - `Mock` serves a deterministic `Scenario` so the whole dashboard runs and is
23//!   debugged with no hardware.
24//! - [`Server`] serves the page, the snapshot, and a live event stream over plain TCP.
25//!
26//! # Capability tiers
27//!
28//! One design serves hardware from a Raspberry Pi to a microcontroller, chosen with a
29//! compile-time tier feature. The `/state` contract is identical across all of them, so a
30//! page written for one tier reads another tier's data:
31//!
32//! - Tiers A and B (`tier-a`, the default, and `tier-b`) embed the full localized app: the
33//!   hand-built visuals, history, authenticated control, and the seed locales. A Tier B build
34//!   trims flash by embedding only the locales it needs (the `locale-*` features, English
35//!   always included); the page learns the embedded set from `GET /locales` and offers only
36//!   those languages.
37//! - Tier C (`tier-c`) embeds only a single self-contained floor page for the smallest
38//!   hardware. It renders the status table with the smallest possible script, and when
39//!   scripting is off entirely it falls back to `GET /lite`, a server-rendered,
40//!   meta-refreshing table with no script at all. It is plain, but it is legible and it
41//!   works on any browser.
42//!
43//! Build a non-default tier with `--no-default-features`, for example
44//! `--no-default-features --features "serve,tier-c"`. Each tier's gzipped page-load budget
45//! is enforced by `cargo xtask dashboard footprint`.
46//!
47//! # Examples
48//!
49//! A device turns the state it holds into the language-neutral snapshot the page fetches:
50//!
51//! ```
52//! use pamoja_dashboard::{State, Status};
53//!
54//! let state = State {
55//!     orgs: Vec::new(),
56//!     status: Status::Alarm,
57//!     uptime_secs: Some(3600),
58//!     demo: false,
59//! };
60//!
61//! let json = state.to_json().expect("serialize");
62//! assert!(json.contains("\"status\":\"alarm\""));
63//! ```
64//!
65//! The hardware-free `Mock` fleet (the `mock` feature) implements [`StateSource`] the same
66//! way a real node does, so the whole dashboard runs and is debugged with no hardware.
67
68mod assets;
69mod command;
70mod source;
71mod state;
72
73#[cfg(feature = "mock")]
74mod mock;
75
76#[cfg(feature = "serve")]
77mod auth;
78#[cfg(feature = "serve")]
79mod catalog;
80#[cfg(feature = "serve")]
81mod fleet;
82#[cfg(feature = "serve")]
83mod lite;
84#[cfg(feature = "serve")]
85mod serve;
86
87// Capability tier is a compile-time choice. The full bundle ships unless `tier-c` is set, so
88// the documented real build (`--no-default-features --features serve`) keeps the full page;
89// `tier-c` embeds only the minimal floor page. Selecting `tier-c` alongside a full tier is
90// ambiguous about which page to embed, so it is rejected with a clear pointer.
91#[cfg(all(feature = "tier-c", any(feature = "tier-a", feature = "tier-b")))]
92compile_error!(
93    "select one dashboard tier: build a non-default tier with --no-default-features, \
94     e.g. --no-default-features --features \"serve,tier-c\""
95);
96
97pub use assets::Assets;
98pub use command::{Command, CommandError};
99// The presentation vocabulary a profile uses to declare custom dashboard elements, so a
100// gateway can build a catalog and pin a reading's graphic from this one crate.
101pub use pamoja_profile::{ElementSpec, Presentation, Scope, Theme, Viz};
102pub use source::StateSource;
103pub use state::{
104    EventLevel, EventRecord, Group, Link, LinkKind, Mode, Org, Reading, Sensor, State, Status,
105    Trend,
106};
107
108#[cfg(feature = "mock")]
109pub use mock::{Mock, Scenario};
110
111#[cfg(feature = "serve")]
112pub use auth::{Auth, AuthError, Challenge};
113#[cfg(feature = "serve")]
114pub use catalog::Catalog;
115#[cfg(feature = "serve")]
116pub use fleet::{Fleet, FleetBuilder};
117#[cfg(feature = "serve")]
118pub use serve::Server;