brewlog/src/presentation/cli/mod.rs
Jon Seager e01b6d0a1c
feat: add optional created_at to entity creation and updates
Add `created_at: Option<DateTime<Utc>>` through all layers so CLI users
can backdate entities at creation/update time. When omitted, falls back
to `Utc::now()`.

- Domain: add field to all New*/Update* structs with serde(default)
- Domain: timeline events use entity created_at instead of Utc::now()
- Repos: unify INSERT to explicit Rust-side created_at with unwrap_or_else
- Repos: add created_at to UPDATE dynamic query builders
- Routes: add field to submission structs and has_changes guards
- Clients: pass created_at through manual JSON client methods
- CLI: add --created-at flag with parse_created_at helper (RFC 3339 or YYYY-MM-DD)
2026-02-07 09:48:20 +00:00

153 lines
3.3 KiB
Rust

pub mod backup;
pub mod bags;
pub mod brews;
pub mod cafes;
pub mod cups;
pub mod gear;
mod macros;
pub mod roasters;
pub mod roasts;
pub mod tokens;
use std::net::SocketAddr;
use chrono::{DateTime, NaiveDate, Utc};
use backup::{BackupCommand, RestoreCommand};
use bags::BagCommands;
use brews::BrewCommands;
use cafes::CafeCommands;
use clap::{Args, Parser, Subcommand};
use cups::CupCommands;
use gear::GearCommands;
use roasters::RoasterCommands;
use roasts::RoastCommands;
use tokens::TokenCommands;
#[derive(Debug, Parser)]
#[command(author, version, about = "Track coffee roasts, brews, and cups", long_about = None)]
pub struct Cli {
#[arg(
long,
global = true,
env = "BREWLOG_URL",
default_value = "http://localhost:3000"
)]
pub api_url: String,
#[command(subcommand)]
pub command: Commands,
}
#[derive(Debug, Subcommand)]
pub enum Commands {
/// Run the HTTP server
Serve(ServeCommand),
/// Manage roasters
Roaster {
#[command(subcommand)]
command: RoasterCommands,
},
/// Manage roasts
Roast {
#[command(subcommand)]
command: RoastCommands,
},
/// Manage bags
Bag {
#[command(subcommand)]
command: BagCommands,
},
/// Manage gear
Gear {
#[command(subcommand)]
command: GearCommands,
},
/// Manage brews
Brew {
#[command(subcommand)]
command: BrewCommands,
},
/// Manage cafes
Cafe {
#[command(subcommand)]
command: CafeCommands,
},
/// Manage cups
Cup {
#[command(subcommand)]
command: CupCommands,
},
/// Manage API tokens
Token {
#[command(subcommand)]
command: TokenCommands,
},
/// Back up all coffee data to JSON (stdout)
Backup(BackupCommand),
/// Restore coffee data from a JSON backup file
Restore(RestoreCommand),
}
#[derive(Debug, Args)]
pub struct ServeCommand {
#[arg(
long,
env = "BREWLOG_DATABASE_URL",
default_value = "sqlite://brewlog.db"
)]
pub database_url: String,
#[arg(long, env = "BREWLOG_BIND_ADDRESS", default_value = "127.0.0.1:3000")]
pub bind_address: SocketAddr,
#[arg(long, env = "BREWLOG_RP_ID")]
pub rp_id: Option<String>,
#[arg(long, env = "BREWLOG_RP_ORIGIN")]
pub rp_origin: Option<String>,
#[arg(long, env = "BREWLOG_OPENROUTER_API_KEY")]
pub openrouter_api_key: Option<String>,
#[arg(
long,
env = "BREWLOG_OPENROUTER_MODEL",
default_value = "openrouter/free"
)]
pub openrouter_model: String,
#[arg(long, env = "BREWLOG_FOURSQUARE_API_KEY")]
pub foursquare_api_key: Option<String>,
}
pub fn parse_created_at(value: &str) -> anyhow::Result<DateTime<Utc>> {
if let Ok(dt) = DateTime::parse_from_rfc3339(value) {
return Ok(dt.with_timezone(&Utc));
}
if let Ok(date) = NaiveDate::parse_from_str(value, "%Y-%m-%d") {
return Ok(date.and_time(chrono::NaiveTime::MIN).and_utc());
}
anyhow::bail!(
"invalid date format: expected RFC 3339 (e.g. 2025-08-05T10:00:00Z) or YYYY-MM-DD"
)
}
pub(crate) fn print_json<T>(value: &T) -> anyhow::Result<()>
where
T: serde::Serialize,
{
let rendered = serde_json::to_string_pretty(value)?;
println!("{rendered}");
Ok(())
}