depot-services-review

Depot Services Review Skill

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "depot-services-review" with this command: npx skills add ecto/muni/ecto-muni-depot-services-review

Depot Services Review Skill

Reviews Rust microservices in the depot/ directory that provide base station infrastructure for rover fleet operations.

Overview

The depot consists of 5 Rust-based microservices that handle rover discovery, mission dispatch, mapping, and GPS infrastructure:

Services Covered

Service Port Purpose Database WebSocket

discovery 4860 Rover registration, heartbeat tracking, session API No Yes (operator updates)

dispatch 4890 Mission planning, task assignment, zone management PostgreSQL Yes (rover + console)

map-api 4870 Serve processed maps and sessions No No

gps-status 4880 RTK base station monitoring No Yes (console updates)

mapper

Map processing orchestrator (batch job) No No

Shared Technology Stack

All services use a consistent technology stack:

Runtime & Async:

  • Rust Edition 2021

  • Tokio 1.41+ (async runtime with rt-multi-thread , macros , net )

  • Futures 0.3 (async utilities)

Web Framework:

  • Axum 0.7-0.8 (web server, routing, WebSocket)

  • Tower-HTTP 0.6 (middleware: CORS, compression, tracing)

  • Hyper (underlying HTTP implementation)

Database (dispatch only):

  • SQLx 0.8 (async PostgreSQL client)

  • PostgreSQL 16+ (database server in Docker)

  • JSON/JSONB for flexible schema fields

Serialization:

  • Serde 1.0 (derive macros)

  • serde_json 1.0 (JSON encoding/decoding)

Error Handling:

  • thiserror 2.0 (library error types)

  • anyhow (application-level error handling)

Logging:

  • tracing 0.1 (structured logging)

  • tracing-subscriber 0.3 (log formatting, env filtering)

Other:

  • UUID 1.0 (unique identifiers with v4 generation)

  • chrono 0.4 (timestamps with serde support)

Architecture Pattern

All services follow a consistent structure:

depot/<service>/ ├── Cargo.toml # Dependencies and metadata ├── Dockerfile # Multi-stage build (alpine-based) ├── src/ │ └── main.rs # Single-file service (300-1200 LOC) └── migrations/ # SQL migrations (dispatch only) └── 001_initial.sql

Code Structure (main.rs):

//! Service documentation

// Imports use axum::{...}; use tokio::...;

// Type definitions struct AppState { ... } type SharedState = Arc<AppState>;

// Main function #[tokio::main] async fn main() { // 1. Initialize logging // 2. Load configuration from env vars // 3. Initialize state (DB connection, channels, etc.) // 4. Create router with routes // 5. Bind to port and serve }

// Handler functions async fn endpoint_handler(...) -> impl IntoResponse { ... }

// Helper functions fn utility_function(...) { ... }

Shared Patterns Review Checklist

  1. Axum Web Server Setup
  • Tokio runtime is configured correctly

  • Uses #[tokio::main] macro for async main

  • Runtime features include at least: rt-multi-thread , macros , net

  • No blocking operations in async contexts

❌ BAD:

fn main() { let rt = tokio::runtime::Runtime::new().unwrap(); rt.block_on(async { ... }); }

✅ GOOD:

#[tokio::main] async fn main() { // Tokio runtime handles everything }

  • Router is created with all routes defined

  • Uses Router::new() with chained .route() calls

  • Groups related routes together (RESTful patterns)

  • WebSocket routes use get() method with ws.on_upgrade()

  • State is attached with .with_state()

✅ GOOD:

let app = Router::new() // CRUD for zones .route("/zones", post(create_zone)) .route("/zones", get(list_zones)) .route("/zones/{id}", get(get_zone)) .route("/zones/{id}", put(update_zone)) .route("/zones/{id}", delete(delete_zone)) // WebSocket .route("/ws", get(ws_handler)) // Health check .route("/health", get(health)) .layer(CorsLayer::permissive()) .with_state(state);

  • CORS is configured appropriately

  • Uses CorsLayer::permissive() for internal services

  • Applied as layer via .layer(CorsLayer::permissive())

  • Enables cross-origin requests from console

✅ GOOD:

use tower_http::cors::CorsLayer;

Router::new() .route("/endpoint", get(handler)) .layer(CorsLayer::permissive()) // Allows all origins for internal services

  • Server binds to correct address

  • Port loaded from PORT env var with sensible default

  • Binds to 0.0.0.0 (all interfaces) for Docker compatibility

  • Uses tokio::net::TcpListener for async binding

✅ GOOD:

let port: u16 = std::env::var("PORT") .ok() .and_then(|p| p.parse().ok()) .unwrap_or(4860); // Service-specific default

let addr = SocketAddr::from(([0, 0, 0, 0], port)); let listener = tokio::net::TcpListener::bind(addr).await.unwrap(); axum::serve(listener, app).await.unwrap();

Reference: See api-design-patterns.md for more Axum patterns.

  1. State Management
  • AppState struct contains all shared state

  • Database pool (dispatch only)

  • Broadcast channels for WebSocket updates

  • Configuration (directories, URLs, etc.)

  • Connected client tracking (if applicable)

✅ GOOD:

struct AppState { db: PgPool, // Database connection pool rovers: RwLock<HashMap<String, ConnectedRover>>, // Mutable state broadcast_tx: broadcast::Sender<BroadcastMessage>, // Update channel }

  • State is wrapped in Arc for sharing

  • Uses Arc<AppState> for clone-on-share semantics

  • Type alias SharedState = Arc<AppState> for convenience

  • Handlers receive State<SharedState> extractor

✅ GOOD:

type SharedState = Arc<AppState>;

let state = Arc::new(AppState::new(pool));

async fn handler(State(state): State<SharedState>) -> impl IntoResponse { // Access state here }

  • Concurrent state access uses appropriate synchronization

  • Immutable state: direct access via Arc

  • Mutable state: RwLock for many readers, few writers

  • Channels: broadcast for fan-out, mpsc for point-to-point

  • Never use Mutex around async operations

❌ BAD:

let rovers = state.rovers.lock().await; // Mutex in async = deadlock risk tokio::time::sleep(Duration::from_secs(5)).await; // Holding lock across await! drop(rovers);

✅ GOOD:

// Read let rovers = state.rovers.read().await; let count = rovers.len(); drop(rovers); // Release lock quickly

// Write let mut rovers = state.rovers.write().await; rovers.insert(id, rover); drop(rovers); // Release lock quickly

  1. Database Patterns (Dispatch Service)
  • SQLx connection pool is created correctly

  • Uses PgPoolOptions::new() to configure pool

  • Sets max_connections appropriately (10-20 for services)

  • Connection URL from DATABASE_URL env var

  • Handles connection errors gracefully

✅ GOOD:

let database_url = std::env::var("DATABASE_URL") .expect("DATABASE_URL must be set");

let pool = PgPoolOptions::new() .max_connections(10) .connect(&database_url) .await .expect("Failed to connect to database");

  • Migrations are applied on startup

  • Migration SQL files in migrations/ directory

  • Applied manually in run_migrations() function

  • Checks if migration already applied before running

  • Logs migration status

✅ GOOD:

async fn run_migrations(pool: &PgPool) { let migration = include_str!("../migrations/001_initial.sql");

// Check if already migrated
let table_exists: bool = sqlx::query_scalar(
    "SELECT EXISTS (SELECT FROM information_schema.tables WHERE table_name = 'zones')"
)
.fetch_one(pool)
.await
.unwrap_or(false);

if !table_exists {
    info!("Running migration...");
    sqlx::raw_sql(migration).execute(pool).await.expect("Migration failed");
}

}

  • Database queries use parameterized queries

  • NEVER use string concatenation for SQL

  • Always use .bind() for parameters

  • Prevents SQL injection vulnerabilities

❌ BAD (SQL injection vulnerability):

let query = format!("SELECT * FROM zones WHERE id = '{}'", user_input); sqlx::query(&query).fetch_one(&pool).await?;

✅ GOOD:

sqlx::query_as::<_, Zone>( "SELECT * FROM zones WHERE id = $1" ) .bind(id) // Parameterized - safe! .fetch_one(&pool) .await?

  • Query results use appropriate methods

  • .fetch_one()

  • exactly one row expected, error if zero or multiple

  • .fetch_optional()

  • zero or one row, returns Option<T>

  • .fetch_all()

  • multiple rows, returns Vec<T>

  • .execute()

  • for INSERT/UPDATE/DELETE, returns rows affected

✅ GOOD:

// Get by ID - must exist let zone: Zone = sqlx::query_as("SELECT ... WHERE id = $1") .bind(id) .fetch_one(&state.db) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;

// Get by ID - may not exist let zone: Option<Zone> = sqlx::query_as("SELECT ... WHERE id = $1") .bind(id) .fetch_optional(&state.db) .await?;

  • JSONB fields are handled correctly

  • Use #[sqlx(json)] attribute on struct fields

  • Serialize/deserialize with serde

  • Type must be serde_json::Value in database model

  • Cast to concrete types when needed

✅ GOOD:

#[derive(FromRow, Serialize)] struct Zone { pub id: Uuid, #[sqlx(json)] pub waypoints: serde_json::Value, // Stored as JSONB }

// When parsing let waypoints: Vec<Waypoint> = serde_json::from_value(zone.waypoints)?;

Reference: See database-patterns.md for comprehensive database guidance.

  1. Error Handling
  • Handler errors use Result with IntoResponse

  • Return type: Result<impl IntoResponse, (StatusCode, String)>

  • Errors map to HTTP status codes

  • Error messages are user-friendly (not debug strings)

✅ GOOD:

async fn get_zone( State(state): State<SharedState>, Path(id): Path<Uuid>, ) -> Result<impl IntoResponse, (StatusCode, String)> { let zone = sqlx::query_as("...") .bind(id) .fetch_optional(&state.db) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))? .ok_or((StatusCode::NOT_FOUND, "Zone not found".to_string()))?;

Ok(Json(zone))

}

  • Custom error types implement IntoResponse

  • Derive from thiserror::Error for libraries

  • Implement IntoResponse for Axum handlers

  • Map error variants to appropriate HTTP status codes

✅ GOOD:

use thiserror::Error;

#[derive(Error, Debug)] pub enum ApiError { #[error("Not found: {0}")] NotFound(String), #[error("IO error: {0}")] Io(#[from] std::io::Error), }

impl IntoResponse for ApiError { fn into_response(self) -> axum::response::Response { let (status, message) = match &self { ApiError::NotFound(msg) => (StatusCode::NOT_FOUND, msg.clone()), ApiError::Io(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()), }; (status, Json(serde_json::json!({ "error": message }))).into_response() } }

  • Database errors are handled appropriately

  • .map_err() converts SQLx errors to HTTP responses

  • Use .ok_or() to convert Option to Result with NOT_FOUND

  • Log errors before returning them

✅ GOOD:

let zone: Zone = sqlx::query_as("SELECT ... WHERE id = $1") .bind(id) .fetch_optional(&state.db) .await .map_err(|e| { warn!(error = %e, "Database query failed"); (StatusCode::INTERNAL_SERVER_ERROR, "Database error".to_string()) })? .ok_or_else(|| { warn!(zone_id = %id, "Zone not found"); (StatusCode::NOT_FOUND, "Zone not found".to_string()) })?;

  1. WebSocket Communication
  • WebSocket upgrade is handled correctly

  • Handler accepts WebSocketUpgrade extractor

  • Returns ws.on_upgrade(|socket| handler_fn(socket, state))

  • Handler function is async fn taking WebSocket and state

✅ GOOD:

async fn ws_handler( ws: WebSocketUpgrade, State(state): State<SharedState>, ) -> impl IntoResponse { ws.on_upgrade(|socket| handle_ws(socket, state)) }

async fn handle_ws(socket: WebSocket, state: SharedState) { let (mut sender, mut receiver) = socket.split(); // ... WebSocket logic }

  • WebSocket messages are properly parsed

  • Use serde_json::from_str() to parse text messages

  • Handle parse errors gracefully (don't crash connection)

  • Implement message protocol with tagged enum

✅ GOOD:

#[derive(Deserialize)] #[serde(tag = "type", rename_all = "lowercase")] enum RoverMessage { Register { rover_id: String }, Progress { task_id: Uuid, progress: i32 }, }

// In handler while let Some(msg) = receiver.next().await { let msg = match msg { Ok(Message::Text(text)) => text, Ok(Message::Close(_)) => break, _ => continue, };

match serde_json::from_str::&#x3C;RoverMessage>(&#x26;msg) {
    Ok(RoverMessage::Register { rover_id }) => { ... },
    Ok(RoverMessage::Progress { task_id, progress }) => { ... },
    Err(e) => warn!(error = %e, "Failed to parse message"),
}

}

  • Broadcast channels are used correctly

  • Create channel with broadcast::channel(capacity)

  • Subscribe with .subscribe() in each WebSocket handler

  • Send updates with .send() (returns Result )

  • Handle lagged receivers (broadcast can drop messages)

✅ GOOD:

// In AppState struct AppState { broadcast_tx: broadcast::Sender<BroadcastMessage>, }

impl AppState { fn new() -> Self { let (broadcast_tx, _rx) = broadcast::channel(256); // Drop receiver Self { broadcast_tx } }

fn broadcast(&#x26;self, msg: BroadcastMessage) {
    let _ = self.broadcast_tx.send(msg);  // Ignore if no receivers
}

}

// In WebSocket handler let mut rx = state.broadcast_tx.subscribe(); loop { tokio::select! { Ok(msg) = rx.recv() => { // Send to client } msg = receiver.next() => { // Handle incoming message } } }

  • WebSocket cleanup is handled on disconnect

  • Remove client from tracking map

  • Abort spawned tasks

  • Broadcast disconnect event if needed

  • No panics in cleanup code

✅ GOOD:

async fn handle_ws(socket: WebSocket, state: SharedState) { let mut rover_id: Option<String> = None;

// WebSocket loop
while let Some(msg) = receiver.next().await {
    // ... handle messages
    if let RoverMessage::Register { rover_id: id } = parsed {
        rover_id = Some(id.clone());
        // Register rover
    }
}

// Cleanup on disconnect
if let Some(id) = rover_id {
    info!(rover_id = %id, "Client disconnected");
    let mut rovers = state.rovers.write().await;
    rovers.remove(&#x26;id);
    drop(rovers);

    state.broadcast(BroadcastMessage::Disconnected { rover_id: id });
}

}

Reference: See websocket-patterns.md for detailed WebSocket guidance.

  1. Logging and Observability
  • Tracing is initialized early

  • Use tracing_subscriber::fmt() in main()

  • Set env filter with fallback: RUST_LOG env var or default level

  • Initialize before any other operations

✅ GOOD:

#[tokio::main] async fn main() { tracing_subscriber::fmt() .with_env_filter( tracing_subscriber::EnvFilter::try_from_default_env() .unwrap_or_else(|_| "discovery=info,sqlx=warn".into()) ) .init();

info!("Service starting...");
// ... rest of main

}

  • Log statements use structured logging

  • Use info! , warn! , error! , debug! macros

  • Include context with key-value pairs

  • Use % for Display, ? for Debug formatting

  • Log important events: startup, requests, errors, shutdown

✅ GOOD:

info!( rover_id = %rover.id, address = %rover.address, "Rover registered" );

warn!( task_id = %task_id, error = %error, "Task failed" );

  • Health check endpoint exists

  • Endpoint at /health returning JSON

  • Includes service status and metrics

  • Returns 200 OK if service is healthy

  • Used by Docker healthcheck

✅ GOOD:

async fn health(State(state): State<SharedState>) -> impl IntoResponse { let rover_count = state.rovers.read().await.len(); Json(serde_json::json!({ "status": "ok", "rovers": rover_count })) }

  1. Configuration and Environment
  • Configuration loaded from environment variables

  • Use std::env::var() for required config

  • Provide sensible defaults for optional config

  • Validate configuration early (fail fast)

  • Log configuration values (except secrets)

✅ GOOD:

// Required let database_url = std::env::var("DATABASE_URL") .expect("DATABASE_URL must be set");

// Optional with default let port: u16 = std::env::var("PORT") .ok() .and_then(|p| p.parse().ok()) .unwrap_or(4860);

let sessions_dir = std::env::var("SESSIONS_DIR") .map(PathBuf::from) .unwrap_or_else(|_| PathBuf::from("/data/sessions"));

info!( port = port, sessions_dir = %sessions_dir.display(), "Configuration loaded" );

  • Paths use PathBuf for cross-platform compatibility

  • Never hardcode paths with / or \

  • Use PathBuf::from() to convert from env vars

  • Use .join() to build paths

  • Use .display() for logging paths

✅ GOOD:

let base_dir = PathBuf::from(std::env::var("DATA_DIR").unwrap_or("/data".into())); let sessions_path = base_dir.join("sessions").join(&rover_id);

info!(path = %sessions_path.display(), "Session path");

  1. Docker Build and Deployment
  • Dockerfile uses multi-stage build

  • Stage 1: rust:1.83-alpine for building

  • Stage 2: alpine:3.21 for runtime

  • Minimizes final image size (typically 10-20 MB)

✅ GOOD:

Build stage

FROM rust:1.83-alpine AS builder RUN apk add --no-cache musl-dev WORKDIR /app COPY Cargo.toml Cargo.lock* ./

Dummy build for caching

RUN mkdir src && echo "fn main() {}" > src/main.rs RUN cargo build --release 2>/dev/null || true

Real build

COPY src ./src RUN touch src/main.rs RUN cargo build --release

Runtime stage

FROM alpine:3.21 RUN apk add --no-cache ca-certificates COPY --from=builder /app/target/release/[service] /usr/local/bin/[service] ENV PORT=4860 EXPOSE 4860 CMD ["[service]"]

  • Release profile is optimized

  • lto = true

  • Link-time optimization

  • opt-level = "z"

  • Optimize for size

  • strip = true

  • Remove debug symbols

✅ GOOD in Cargo.toml:

[profile.release] lto = true opt-level = "z" strip = true

  • Docker Compose service is configured correctly

  • Container name follows pattern: depot-<service>

  • Restart policy: unless-stopped

  • Ports mapped correctly (host:container)

  • Environment variables passed from .env

  • Healthcheck defined

  • Dependencies declared with depends_on

✅ GOOD in docker-compose.yml:

discovery: build: context: ./discovery dockerfile: Dockerfile container_name: depot-discovery restart: unless-stopped ports: - "4860:4860" environment: - PORT=4860 - RUST_LOG=discovery=info healthcheck: test: ["CMD", "wget", "-q", "-O-", "http://localhost:4860/health"] interval: 10s timeout: 3s retries: 3

Reference: See docker-deployment.md for comprehensive Docker patterns.

Service-Specific Reviews

Discovery Service (depot/discovery)

Purpose: Rover registration, heartbeat tracking, session file serving

Key Files:

  • src/main.rs

  • Main service implementation (~600 LOC)

  • Dockerfile

  • Multi-stage build

Endpoints:

  • POST /register

  • Register rover with metadata

  • POST /heartbeat/{id}

  • Update rover status with telemetry

  • GET /rovers

  • List all registered rovers (HTTP fallback)

  • GET /ws

  • WebSocket for live rover updates to operators

  • GET /api/sessions

  • List recorded sessions

  • GET /api/sessions/{rover_id}/{session_dir}/session.rrd

  • Serve session file

  • GET /health

  • Health check

Specific Concerns:

Rover timeout is implemented correctly

  • Constant ROVER_TIMEOUT = Duration::from_secs(10)

  • Background task checks for stale rovers every 2 seconds

  • Online status computed on-the-fly in get_rovers()

Session file serving is secure

  • Path traversal attacks prevented (no .. in paths)

  • Files served from configured SESSIONS_DIR only

  • Tries both direct and nested session structures

  • Returns 404 if file doesn't exist

WebSocket updates are efficient

  • Initial rover list sent on connection

  • Updates only sent when state changes

  • Broadcast channel prevents blocking on slow clients

Common Issues:

  • Forgetting to call state.notify() after state changes

  • Not dropping read locks before broadcasting (potential deadlock)

  • Not handling both session directory structures (direct vs nested)

Dispatch Service (depot/dispatch)

Purpose: Mission planning, zone management, task assignment to rovers

Key Files:

  • src/main.rs

  • Main service implementation (~1200 LOC)

  • migrations/001_initial.sql

  • Database schema

  • Dockerfile

  • Multi-stage build

Database Tables:

  • zones

  • Geographic areas (routes, polygons, points)

  • missions

  • Scheduled work definitions

  • tasks

  • Execution instances with progress tracking

Endpoints:

  • POST/GET/PUT/DELETE /zones

  • Zone CRUD

  • POST/GET/PUT/DELETE /missions

  • Mission CRUD

  • POST /missions/{id}/start

  • Start mission (creates task, assigns to rover)

  • POST /missions/{id}/stop

  • Stop mission (cancels active task)

  • GET/POST /tasks

  • Task management

  • GET /ws

  • WebSocket for rover connections

  • GET /ws/console

  • WebSocket for console updates

  • GET /health

  • Health check

Specific Concerns:

Task lifecycle is managed correctly

  • States: pending → assigned → active → done/failed/cancelled

  • started_at set on first progress update

  • ended_at set when task completes/fails/cancelled

  • Rover's current_task cleared when task ends

WebSocket protocol is implemented correctly

  • Two WebSocket endpoints: /ws (rovers), /ws/console (operators)

  • Rover messages: Register , Progress , Complete , Failed

  • Dispatch messages: Task , Cancel

  • Broadcast messages: TaskUpdate , RoverUpdate , ZoneUpdate , MissionUpdate

Task assignment logic is correct

  • Check if mission has preferred rover, verify connected

  • If no preference, find any available rover (no current task)

  • Create task in database with status=assigned

  • Update rover's current_task in memory

  • Send task to rover via WebSocket

  • Rollback if send fails

JSONB fields are validated

  • zones.waypoints contains array of {x, y, theta?} objects

  • missions.schedule contains {trigger, cron?, loop} object

  • Validate structure before inserting (prevent invalid data)

Common Issues:

  • Not clearing rover.current_task when task ends (rover stuck)

  • Forgetting to broadcast updates to console clients

  • Not rolling back database changes if WebSocket send fails

  • Race condition between task creation and rover disconnect

Map API Service (depot/map-api)

Purpose: Serve processed maps and 3D assets to console and other clients

Key Files:

  • src/main.rs

  • Main service implementation (~450 LOC)

  • Dockerfile

  • Multi-stage build

Endpoints:

  • GET /maps

  • List all maps

  • GET /maps/{id}

  • Get map manifest (metadata)

  • GET /maps/{id}/{asset}

  • Download asset (splat.ply, pointcloud.laz, mesh.glb, thumbnail.jpg)

  • GET /sessions

  • List all sessions

  • GET /sessions/{id}

  • Get session metadata

  • GET /maps/{id}/sessions

  • Get sessions used to build map

  • GET /health

  • Health check

Specific Concerns:

Map manifest loading is efficient

  • Reads maps/index.json for map list

  • Lazily loads manifests from maps/{name}/manifest.json

  • Caches manifests in RwLock<HashMap<Uuid, MapManifest>>

  • Reloads on each list/get request (eventual consistency)

Asset serving is correct

  • Validates asset exists in manifest before serving

  • Sets correct Content-Type header for each asset type

  • Reads entire file into memory (acceptable for small assets)

  • Returns 404 if asset file missing

File paths are constructed safely

  • Uses PathBuf::join() to build paths

  • Validates file exists before serving

  • No path traversal vulnerabilities

Common Issues:

  • Serving assets not listed in manifest (security issue)

  • Not setting Content-Type header (browser confusion)

  • Not handling missing files gracefully

GPS Status Service (depot/gps-status)

Purpose: Monitor RTK base station status and broadcast to console

Key Files:

  • src/main.rs

  • Main service implementation (~400 LOC)

  • Dockerfile

  • Multi-stage build

Endpoints:

  • GET /status

  • Current RTK base station status

  • GET /ws

  • WebSocket for live status updates to console

  • GET /health

  • Health check

Specific Concerns:

RTK status is polled correctly

  • Background task polls base station via serial/TCP

  • Parses RTCM3/NMEA messages for status

  • Broadcasts status updates via WebSocket

  • Handles base station disconnection gracefully

WebSocket updates are throttled

  • Status sent at reasonable interval (1-5 seconds)

  • Prevents overwhelming clients with updates

  • Initial status sent on connection

Common Issues:

  • Not handling base station disconnection

  • Sending updates too frequently (CPU/bandwidth waste)

  • Not validating RTCM3 message checksums

Mapper Service (depot/mapper)

Purpose: Orchestrate map processing pipeline (batch job, not always running)

Key Files:

  • src/main.rs

  • Main orchestrator (~1000 LOC)

  • Dockerfile

  • Multi-stage build

Operation:

  • Runs as batch job (not a long-running service)

  • Scans sessions directory for new sessions

  • Queues sessions for processing

  • Invokes splat-worker for 3D reconstruction

  • Generates map manifests and assets

  • Updates index.json

Specific Concerns:

Session discovery is efficient

  • Scans filesystem for new sessions

  • Reads metadata.json from each session

  • Filters by GPS bounds, frame counts, etc.

  • Deduplicates sessions already processed

Processing pipeline is fault-tolerant

  • Tracks session status (pending, processing, processed, failed)

  • Retries failed sessions with exponential backoff

  • Logs errors for manual intervention

  • Doesn't block on failed sessions

Map manifest generation is correct

  • Calculates GPS bounds from all sessions

  • Lists all available assets (splat, pointcloud, mesh, thumbnail)

  • Includes session references

  • Updates index.json atomically

Common Issues:

  • Not handling concurrent mapper invocations (file conflicts)

  • Not validating session metadata before processing

  • Not cleaning up temporary files on failure

Quick Commands

Development

Check code (no build)

cargo check -p discovery cargo check -p dispatch

Build service

cargo build -p discovery --release

Run tests

cargo test -p dispatch

Run service locally (requires dependencies)

cd depot/discovery PORT=4860 cargo run

Run with logging

RUST_LOG=discovery=debug cargo run

Docker

Build service image

cd depot/discovery docker build -t depot-discovery .

Run service container

docker run -p 4860:4860 -e RUST_LOG=info depot-discovery

Build all services via Docker Compose

cd depot docker compose build

Start all services

docker compose up -d

View logs

docker compose logs -f discovery

Restart service

docker compose restart dispatch

Stop all services

docker compose down

Database (Dispatch Only)

Connect to PostgreSQL

docker compose exec postgres psql -U postgres -d dispatch

View tables

\dt

Query zones

SELECT id, name, zone_type FROM zones;

Query tasks

SELECT id, status, rover_id, progress FROM tasks ORDER BY created_at DESC LIMIT 10;

Reset database (DESTRUCTIVE)

docker compose down -v # Removes volumes docker compose up -d postgres docker compose restart dispatch # Migrations run on startup

References

  • database-patterns.md - SQLx, migrations, queries, transactions

  • api-design-patterns.md - Axum routing, middleware, error handling

  • websocket-patterns.md - WebSocket protocols, broadcast channels

  • docker-deployment.md - Multi-stage builds, Docker Compose, healthchecks

  • CLAUDE.md - Project-wide conventions

  • depot/README.md - Depot architecture overview

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

mcu-embedded-review

No summary provided by upstream source.

Repository SourceNeeds Review
General

firmware-review

No summary provided by upstream source.

Repository SourceNeeds Review
General

integration-testing

No summary provided by upstream source.

Repository SourceNeeds Review