Dashboards Module
The dashboard module (server/src/dashboard/) implements two distinct concerns: routing static Evidence dashboards under /<slug>/..., and scaffolding new dashboard projects on disk from embedded templates. They share a common config type but live in separate submodules so the runtime path can compile without the scaffolder.
server/src/dashboard/
mod.rs // public exports: DashboardsState, Dashboard, DashboardSwap,
// build_dashboard_router, landing_handler
config.rs // runtime types (DashboardsState, Dashboard, ScaffoldInput, ScaffoldResult)
routes.rs // landing_handler, build_dashboard_router, DashboardSwap (tower::Service)
scaffold.rs // [feature=cli] scaffold_dashboard_folder + drift detection
templates.rs // [feature=cli] embedded template list + render helpers
templates/ // raw template files (Svelte, TypeScript, npm config, landing page HTML)
Runtime Types
DashboardsState
#![allow(unused)]
fn main() {
pub struct DashboardsState {
pub root: PathBuf, // absolute path to the dashboards root directory
pub dashboards: Vec<Dashboard>,
}
}
Stored inside AppState behind Arc<ArcSwap<DashboardsState>>, so the file watcher can replace it atomically without taking a lock.
Dashboard
#![allow(unused)]
fn main() {
pub struct Dashboard {
pub slug: String,
pub title: String,
pub description: Option<String>,
pub tags: Vec<String>,
pub enabled: bool,
pub folder_path: PathBuf, // absolute path to the project directory
pub build_path: PathBuf, // absolute path to the built static site
}
}
enabled = !disabled from the YAML — disabled entries are kept in state but excluded from the router.
Routing
build_dashboard_router(&[Dashboard]) -> Router walks the enabled dashboards and:
- If
<build_path>/index.htmlexists — mounts atower_http::ServeDirrooted atbuild_path, with an SPA fallback that servesindex.htmlfor any path not found on disk (so client-side routing works for deep links). - If it doesn’t exist — registers a 503 page at
/<slug>and/<slug>/{*rest}instructing the operator to runtiders-x402-server dashboard <slug>and thennpm install && npm run build.
The full router is wrapped in DashboardSwap, a tower::Service that loads the current router via arc-swap.load_full() on every request and forwards via Router::oneshot. This is what makes hot reload of dashboards: lock-free: when the YAML changes, the watcher rebuilds a new Router and stores it in the ArcSwap; the next call to DashboardSwap::call picks up the new pointer while in-flight requests finish on the old one.
Landing Page
landing_handler reads <dashboards_root>/index.html and returns it verbatim. This file is generated by the dashboard subcommand (not at runtime) — it’s a static snapshot of the configured dashboard list. If the file is missing, the handler returns 503 with a hint to scaffold.
The route GET / is only mounted in start_server when state.dashboards.dashboards is non-empty.
Scaffolding
scaffold_dashboard_folder(&ScaffoldInput) -> Result<ScaffoldResult> writes a complete Evidence project to disk:
#![allow(unused)]
fn main() {
pub struct ScaffoldInput<'a> {
pub project_dir: &'a Path,
pub slug: &'a str,
pub title: &'a str,
pub seed_table: &'a str, // first table in tables: — used in the starter index.md
pub source_name: &'a str, // local_duckdb / pg / clickhouse
pub force: bool,
pub rendered_files: Vec<(PathBuf, String)>, // generated connection.yaml + per-table .sql files
}
}
It iterates the embedded TEMPLATES table (Svelte components, npm config, layout, wallet libraries) plus the caller-supplied rendered_files, applying {{SLUG}} / {{TITLE}} / {{SEED_TABLE}} / {{SOURCE_NAME}} substitution to anything marked substitute: true. The user-owned pages/index.md is written only when missing — never on --force.
Every run also writes .tiders-managed.json, a JSON manifest mapping each managed file’s project-relative path to its sha256.
Drift Detection (--force)
Without --force, the scaffolder refuses to touch a non-empty existing directory. With --force:
- Read
.tiders-managed.jsonfrom the previous run. - For each managed file, hash the on-disk version and compare to the recorded value.
- If they match — overwrite silently.
- If they differ — copy the on-disk file to
<project>/.old/<filename>first, log a warning, then overwrite with the new template. - If the file is missing — write fresh, no backup needed.
If .tiders-managed.json is absent or unparseable, every file is treated as unmodified and overwritten without backup.
Templates
Templates are embedded into the binary via include_str!. The list lives in templates.rs::TEMPLATES:
| Path | Substituted? | Purpose |
|---|---|---|
.npmrc, .gitignore | no | npm boilerplate |
package.json, evidence.config.yaml | yes | Evidence project config (slug/title injected) |
pages/+layout.svelte | no | Wraps every page with the wallet connect button + global styles |
components/ConnectButton.svelte | no | EIP-6963 wallet picker entry point |
components/WalletPicker.svelte | no | Wallet discovery UI |
components/TidersDownloadButton.svelte | no | Drop-in <TidersDownloadButton table="…"> for any Markdown page |
components/TidersDownloadModal.svelte | no | Modal that runs the x402 dance and offers the result as a CSV |
components/lib/eip6963.ts | no | Browser wallet discovery (EIP-6963) |
components/lib/wagmi.ts | no | wagmi config (Base Sepolia) |
components/lib/walletStore.ts | no | Svelte store wrapping wagmi state |
components/lib/x402Client.ts | no | fetch wrapper that handles 402 + signing via @x402/evm |
components/lib/arrowToCsv.ts | no | Convert Arrow IPC stream to CSV for download |
STARTER_INDEX_MD is the user-owned pages/index.md — written only on first scaffold so user edits survive every subsequent --force run.
Generated Files
In addition to the embedded templates, the CLI’s dashboard command generates files based on the YAML config:
sources/<source_name>/connection.yaml— Evidence source config built fromdatabase:. For DuckDB thefilenameis computed relative to the source directory, andaccess_mode: READ_ONLYis set so the dashboard can rebuild against a database file the server is also reading.sources/<source_name>/<table>.sql— one file pertables:entry, each containingselect * from <table> limit 10. Evidence’s source step extracts these into Parquet duringnpm run build.<dashboards_root>/index.html— landing page snapshot. Built fromtemplates/landing_page.htmlwith the<!--DASHBOARD_LIST-->placeholder replaced by an HTML list of all enabled dashboards (titles, descriptions, tag pills).
Hot Reload
The CLI’s file watcher (cli/watcher.rs) re-runs resolve_dashboards on each YAML change, builds a new Router via build_dashboard_router, and stores both into the arc-swap slots on AppState. The change is picked up on the next request — no restart, no dropped connections. The GET / route, however, is decided at startup based on whether any dashboards are configured; toggling between zero and one dashboard requires a restart for the landing route to mount or unmount.