Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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.html exists — mounts a tower_http::ServeDir rooted at build_path, with an SPA fallback that serves index.html for 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 run tiders-x402-server dashboard <slug> and then npm 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:

  1. Read .tiders-managed.json from the previous run.
  2. For each managed file, hash the on-disk version and compare to the recorded value.
  3. If they match — overwrite silently.
  4. If they differ — copy the on-disk file to <project>/.old/<filename> first, log a warning, then overwrite with the new template.
  5. 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:

PathSubstituted?Purpose
.npmrc, .gitignorenonpm boilerplate
package.json, evidence.config.yamlyesEvidence project config (slug/title injected)
pages/+layout.sveltenoWraps every page with the wallet connect button + global styles
components/ConnectButton.sveltenoEIP-6963 wallet picker entry point
components/WalletPicker.sveltenoWallet discovery UI
components/TidersDownloadButton.sveltenoDrop-in <TidersDownloadButton table="…"> for any Markdown page
components/TidersDownloadModal.sveltenoModal that runs the x402 dance and offers the result as a CSV
components/lib/eip6963.tsnoBrowser wallet discovery (EIP-6963)
components/lib/wagmi.tsnowagmi config (Base Sepolia)
components/lib/walletStore.tsnoSvelte store wrapping wagmi state
components/lib/x402Client.tsnofetch wrapper that handles 402 + signing via @x402/evm
components/lib/arrowToCsv.tsnoConvert 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 from database:. For DuckDB the filename is computed relative to the source directory, and access_mode: READ_ONLY is set so the dashboard can rebuild against a database file the server is also reading.
  • sources/<source_name>/<table>.sql — one file per tables: entry, each containing select * from <table> limit 10. Evidence’s source step extracts these into Parquet during npm run build.
  • <dashboards_root>/index.html — landing page snapshot. Built from templates/landing_page.html with 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.