max / makenotwork
6 files changed,
+108 insertions,
-79 deletions
| @@ -23,3 +23,35 @@ reqwest = { version = "0.13", default-features = false, features = ["json", "rus | |||
| 23 | 23 | tokio = { version = "1", features = ["macros", "rt-multi-thread", "net", "time"] } | |
| 24 | 24 | urlencoding = "2" | |
| 25 | 25 | uuid = { version = "1", features = ["v4"] } | |
| 26 | + | ||
| 27 | + | [lints.rust] | |
| 28 | + | unused = "warn" | |
| 29 | + | unreachable_pub = "warn" | |
| 30 | + | ||
| 31 | + | [lints.clippy] | |
| 32 | + | pedantic = { level = "warn", priority = -1 } | |
| 33 | + | # Allow-list tuned from a measured breakdown across server/multithreaded/pter | |
| 34 | + | # (2026-07-22). These are the high-churn / low-signal pedantic lints; everything | |
| 35 | + | # else in `pedantic` stays a warning. Keep this block identical across repos. | |
| 36 | + | module_name_repetitions = "allow" | |
| 37 | + | # Doc lints — no docs-completeness push underway. | |
| 38 | + | missing_errors_doc = "allow" | |
| 39 | + | missing_panics_doc = "allow" | |
| 40 | + | doc_markdown = "allow" | |
| 41 | + | # Numeric casts — endemic and mostly intentional in size/byte math. | |
| 42 | + | cast_possible_truncation = "allow" | |
| 43 | + | cast_sign_loss = "allow" | |
| 44 | + | cast_precision_loss = "allow" | |
| 45 | + | cast_possible_wrap = "allow" | |
| 46 | + | cast_lossless = "allow" | |
| 47 | + | # Subjective structure/style nags — high churn, low signal. | |
| 48 | + | must_use_candidate = "allow" | |
| 49 | + | too_many_lines = "allow" | |
| 50 | + | struct_excessive_bools = "allow" | |
| 51 | + | similar_names = "allow" | |
| 52 | + | items_after_statements = "allow" | |
| 53 | + | single_match_else = "allow" | |
| 54 | + | # Frequent false-positives in TUI/router-heavy code — added from the buckets breakdown. | |
| 55 | + | match_same_arms = "allow" | |
| 56 | + | unnecessary_wraps = "allow" | |
| 57 | + | type_complexity = "allow" |
| @@ -21,13 +21,13 @@ use crate::types::{Channel, NewTicket, Priority, Status, Ticket}; | |||
| 21 | 21 | ||
| 22 | 22 | /// Shared state: SQLite connection + node identity. | |
| 23 | 23 | #[derive(Clone)] | |
| 24 | - | pub struct AppState { | |
| 24 | + | pub(crate) struct AppState { | |
| 25 | 25 | pub db: Arc<Mutex<Connection>>, | |
| 26 | 26 | pub node_id: String, | |
| 27 | 27 | } | |
| 28 | 28 | ||
| 29 | 29 | /// Start the HTTP server, optionally syncing with peers. | |
| 30 | - | pub async fn serve( | |
| 30 | + | pub(crate) async fn serve( | |
| 31 | 31 | conn: Connection, | |
| 32 | 32 | port: u16, | |
| 33 | 33 | peers: Vec<String>, | |
| @@ -88,7 +88,7 @@ async fn create_ticket( | |||
| 88 | 88 | } | |
| 89 | 89 | ||
| 90 | 90 | #[derive(Debug, Deserialize, Default)] | |
| 91 | - | pub struct ListQuery { | |
| 91 | + | pub(crate) struct ListQuery { | |
| 92 | 92 | pub status: Option<String>, | |
| 93 | 93 | pub priority: Option<String>, | |
| 94 | 94 | pub channel: Option<String>, | |
| @@ -142,7 +142,7 @@ async fn get_ticket(State(state): State<AppState>, Path(id): Path<String>) -> im | |||
| 142 | 142 | } | |
| 143 | 143 | ||
| 144 | 144 | #[derive(Debug, Deserialize)] | |
| 145 | - | pub struct UpdateBody { | |
| 145 | + | pub(crate) struct UpdateBody { | |
| 146 | 146 | pub status: Option<String>, | |
| 147 | 147 | } | |
| 148 | 148 | ||
| @@ -154,27 +154,21 @@ async fn update_ticket( | |||
| 154 | 154 | ) -> impl IntoResponse { | |
| 155 | 155 | let conn = state.db.lock().await; | |
| 156 | 156 | ||
| 157 | - | let ticket = match db::get_ticket(&conn, &id) { | |
| 158 | - | Ok(t) => t, | |
| 159 | - | Err(_) => { | |
| 160 | - | return ( | |
| 161 | - | StatusCode::NOT_FOUND, | |
| 162 | - | Json(serde_json::json!({"error": "ticket not found"})), | |
| 163 | - | ) | |
| 164 | - | .into_response(); | |
| 165 | - | } | |
| 157 | + | let Ok(ticket) = db::get_ticket(&conn, &id) else { | |
| 158 | + | return ( | |
| 159 | + | StatusCode::NOT_FOUND, | |
| 160 | + | Json(serde_json::json!({"error": "ticket not found"})), | |
| 161 | + | ) | |
| 162 | + | .into_response(); | |
| 166 | 163 | }; | |
| 167 | 164 | ||
| 168 | 165 | if let Some(status_str) = body.status { | |
| 169 | - | let status = match status_str.parse::<Status>() { | |
| 170 | - | Ok(s) => s, | |
| 171 | - | Err(_) => { | |
| 172 | - | return ( | |
| 173 | - | StatusCode::BAD_REQUEST, | |
| 174 | - | Json(serde_json::json!({"error": format!("invalid status: {status_str}")})), | |
| 175 | - | ) | |
| 176 | - | .into_response(); | |
| 177 | - | } | |
| 166 | + | let Ok(status) = status_str.parse::<Status>() else { | |
| 167 | + | return ( | |
| 168 | + | StatusCode::BAD_REQUEST, | |
| 169 | + | Json(serde_json::json!({"error": format!("invalid status: {status_str}")})), | |
| 170 | + | ) | |
| 171 | + | .into_response(); | |
| 178 | 172 | }; | |
| 179 | 173 | if let Err(e) = db::update_status(&conn, &ticket.id, status) { | |
| 180 | 174 | return ( | |
| @@ -198,7 +192,7 @@ async fn update_ticket( | |||
| 198 | 192 | // -- Sync endpoints ----------------------------------------------------------- | |
| 199 | 193 | ||
| 200 | 194 | #[derive(Debug, Deserialize)] | |
| 201 | - | pub struct SyncPullQuery { | |
| 195 | + | pub(crate) struct SyncPullQuery { | |
| 202 | 196 | /// RFC3339 timestamp. Returns tickets updated after this time. | |
| 203 | 197 | pub since: String, | |
| 204 | 198 | } | |
| @@ -338,7 +332,10 @@ async fn sync_with_peer( | |||
| 338 | 332 | .await? | |
| 339 | 333 | .json() | |
| 340 | 334 | .await?; | |
| 341 | - | let accepted = resp.get("accepted").and_then(|v| v.as_u64()).unwrap_or(0); | |
| 335 | + | let accepted = resp | |
| 336 | + | .get("accepted") | |
| 337 | + | .and_then(serde_json::Value::as_u64) | |
| 338 | + | .unwrap_or(0); | |
| 342 | 339 | if accepted > 0 { | |
| 343 | 340 | eprintln!("sync: pushed {accepted} ticket(s) to {peer_url}"); | |
| 344 | 341 | } |
| @@ -6,13 +6,13 @@ use crate::types::{Channel, Priority, Status}; | |||
| 6 | 6 | ||
| 7 | 7 | #[derive(Parser)] | |
| 8 | 8 | #[command(name = "wam", about = "Whack-a-Mole -- distributed ticket manager")] | |
| 9 | - | pub struct Cli { | |
| 9 | + | pub(crate) struct Cli { | |
| 10 | 10 | #[command(subcommand)] | |
| 11 | 11 | pub command: Option<Command>, | |
| 12 | 12 | } | |
| 13 | 13 | ||
| 14 | 14 | #[derive(Subcommand)] | |
| 15 | - | pub enum Command { | |
| 15 | + | pub(crate) enum Command { | |
| 16 | 16 | /// Create a new ticket | |
| 17 | 17 | Create { | |
| 18 | 18 | /// Ticket title | |
| @@ -103,13 +103,13 @@ pub enum Command { | |||
| 103 | 103 | } | |
| 104 | 104 | ||
| 105 | 105 | #[derive(Clone, Copy, Debug, clap::ValueEnum)] | |
| 106 | - | pub enum ExportFormat { | |
| 106 | + | pub(crate) enum ExportFormat { | |
| 107 | 107 | Json, | |
| 108 | 108 | Csv, | |
| 109 | 109 | } | |
| 110 | 110 | ||
| 111 | 111 | /// Parse a duration string like `90d`, `12h`, `30m` into `chrono::Duration`. | |
| 112 | - | pub fn parse_duration(s: &str) -> Result<chrono::Duration, String> { | |
| 112 | + | pub(crate) fn parse_duration(s: &str) -> Result<chrono::Duration, String> { | |
| 113 | 113 | let s = s.trim(); | |
| 114 | 114 | if s.is_empty() { | |
| 115 | 115 | return Err("duration is empty".to_string()); |
| @@ -1,5 +1,7 @@ | |||
| 1 | 1 | //! SQLite data layer for tickets with sync support. | |
| 2 | 2 | ||
| 3 | + | use std::fmt::Write as _; | |
| 4 | + | ||
| 3 | 5 | use chrono::{DateTime, Utc}; | |
| 4 | 6 | use color_eyre::eyre::{Result, WrapErr, eyre}; | |
| 5 | 7 | use rusqlite::{Connection, Row, params}; | |
| @@ -7,7 +9,7 @@ use rusqlite::{Connection, Row, params}; | |||
| 7 | 9 | use crate::types::{Channel, NewTicket, Priority, Status, Ticket}; | |
| 8 | 10 | ||
| 9 | 11 | /// Open (or create) the WAM database and run migrations. | |
| 10 | - | pub fn open_db() -> Result<Connection> { | |
| 12 | + | pub(crate) fn open_db() -> Result<Connection> { | |
| 11 | 13 | let dirs = directories::ProjectDirs::from("work", "makenot", "wam") | |
| 12 | 14 | .ok_or_else(|| eyre!("cannot determine data directory"))?; | |
| 13 | 15 | let data_dir = dirs.data_dir(); | |
| @@ -25,7 +27,7 @@ pub fn open_db() -> Result<Connection> { | |||
| 25 | 27 | ||
| 26 | 28 | /// Open an in-memory database for testing. | |
| 27 | 29 | #[cfg(test)] | |
| 28 | - | pub fn open_memory() -> Result<Connection> { | |
| 30 | + | pub(crate) fn open_memory() -> Result<Connection> { | |
| 29 | 31 | let conn = Connection::open_in_memory()?; | |
| 30 | 32 | migrate(&conn)?; | |
| 31 | 33 | Ok(conn) | |
| @@ -95,7 +97,7 @@ fn migrate(conn: &Connection) -> Result<()> { | |||
| 95 | 97 | } | |
| 96 | 98 | ||
| 97 | 99 | /// Get or create this node's persistent identity. | |
| 98 | - | pub fn get_or_create_node_id(conn: &Connection) -> Result<String> { | |
| 100 | + | pub(crate) fn get_or_create_node_id(conn: &Connection) -> Result<String> { | |
| 99 | 101 | let existing: Option<String> = conn | |
| 100 | 102 | .query_row("SELECT value FROM meta WHERE key = 'node_id'", [], |row| { | |
| 101 | 103 | row.get(0) | |
| @@ -133,11 +135,9 @@ fn row_to_ticket(row: &Row) -> rusqlite::Result<Ticket> { | |||
| 133 | 135 | source: row.get("source")?, | |
| 134 | 136 | source_ref: row.get("source_ref")?, | |
| 135 | 137 | created_at: DateTime::parse_from_rfc3339(&created_str) | |
| 136 | - | .map(|dt| dt.with_timezone(&Utc)) | |
| 137 | - | .unwrap_or_else(|_| Utc::now()), | |
| 138 | + | .map_or_else(|_| Utc::now(), |dt| dt.with_timezone(&Utc)), | |
| 138 | 139 | updated_at: DateTime::parse_from_rfc3339(&updated_str) | |
| 139 | - | .map(|dt| dt.with_timezone(&Utc)) | |
| 140 | - | .unwrap_or_else(|_| Utc::now()), | |
| 140 | + | .map_or_else(|_| Utc::now(), |dt| dt.with_timezone(&Utc)), | |
| 141 | 141 | resolved_at: resolved_str.and_then(|s| { | |
| 142 | 142 | DateTime::parse_from_rfc3339(&s) | |
| 143 | 143 | .map(|dt| dt.with_timezone(&Utc)) | |
| @@ -147,7 +147,7 @@ fn row_to_ticket(row: &Row) -> rusqlite::Result<Ticket> { | |||
| 147 | 147 | } | |
| 148 | 148 | ||
| 149 | 149 | /// Create a new ticket. Returns the created ticket. | |
| 150 | - | pub fn create_ticket(conn: &Connection, new: &NewTicket, node_id: &str) -> Result<Ticket> { | |
| 150 | + | pub(crate) fn create_ticket(conn: &Connection, new: &NewTicket, node_id: &str) -> Result<Ticket> { | |
| 151 | 151 | let id = uuid::Uuid::new_v4().to_string(); | |
| 152 | 152 | let now = Utc::now().to_rfc3339(); | |
| 153 | 153 | ||
| @@ -172,7 +172,7 @@ pub fn create_ticket(conn: &Connection, new: &NewTicket, node_id: &str) -> Resul | |||
| 172 | 172 | } | |
| 173 | 173 | ||
| 174 | 174 | /// Get a ticket by exact ID or unique prefix match. | |
| 175 | - | pub fn get_ticket(conn: &Connection, id_prefix: &str) -> Result<Ticket> { | |
| 175 | + | pub(crate) fn get_ticket(conn: &Connection, id_prefix: &str) -> Result<Ticket> { | |
| 176 | 176 | let mut stmt = conn.prepare("SELECT * FROM tickets WHERE id LIKE ?1 || '%'")?; | |
| 177 | 177 | let tickets: Vec<Ticket> = stmt | |
| 178 | 178 | .query_map(params![id_prefix], row_to_ticket)? | |
| @@ -187,7 +187,7 @@ pub fn get_ticket(conn: &Connection, id_prefix: &str) -> Result<Ticket> { | |||
| 187 | 187 | ||
| 188 | 188 | /// Filter criteria for listing tickets. | |
| 189 | 189 | #[derive(Default)] | |
| 190 | - | pub struct ListFilter<'a> { | |
| 190 | + | pub(crate) struct ListFilter<'a> { | |
| 191 | 191 | pub status: Option<Status>, | |
| 192 | 192 | /// Filter by painhours color band (see [`Priority`]). | |
| 193 | 193 | pub priority: Option<Priority>, | |
| @@ -202,25 +202,25 @@ pub struct ListFilter<'a> { | |||
| 202 | 202 | /// The painhours score depends on the current time and the resolve-freeze rule, | |
| 203 | 203 | /// so it can't be expressed in SQL's `ORDER BY`; the band filter and ranking are | |
| 204 | 204 | /// applied in Rust after the row-level filters run in the query. | |
| 205 | - | pub fn list_tickets(conn: &Connection, filter: &ListFilter) -> Result<Vec<Ticket>> { | |
| 205 | + | pub(crate) fn list_tickets(conn: &Connection, filter: &ListFilter) -> Result<Vec<Ticket>> { | |
| 206 | 206 | let mut sql = String::from("SELECT * FROM tickets WHERE 1=1"); | |
| 207 | 207 | let mut bind_values: Vec<String> = Vec::new(); | |
| 208 | 208 | ||
| 209 | 209 | if let Some(status) = filter.status { | |
| 210 | 210 | bind_values.push(status.to_string()); | |
| 211 | - | sql.push_str(&format!(" AND status = ?{}", bind_values.len())); | |
| 211 | + | let _ = write!(sql, " AND status = ?{}", bind_values.len()); | |
| 212 | 212 | } | |
| 213 | 213 | if let Some(channel) = filter.channel { | |
| 214 | 214 | bind_values.push(channel.to_string()); | |
| 215 | - | sql.push_str(&format!(" AND channel = ?{}", bind_values.len())); | |
| 215 | + | let _ = write!(sql, " AND channel = ?{}", bind_values.len()); | |
| 216 | 216 | } | |
| 217 | 217 | if let Some(source) = filter.source { | |
| 218 | 218 | bind_values.push(source.to_string()); | |
| 219 | - | sql.push_str(&format!(" AND source = ?{}", bind_values.len())); | |
| 219 | + | let _ = write!(sql, " AND source = ?{}", bind_values.len()); | |
| 220 | 220 | } | |
| 221 | 221 | if let Some(search) = filter.search { | |
| 222 | 222 | bind_values.push(format!("%{search}%")); | |
| 223 | - | sql.push_str(&format!(" AND title LIKE ?{}", bind_values.len())); | |
| 223 | + | let _ = write!(sql, " AND title LIKE ?{}", bind_values.len()); | |
| 224 | 224 | } | |
| 225 | 225 | ||
| 226 | 226 | let mut stmt = conn.prepare(&sql)?; | |
| @@ -247,7 +247,7 @@ pub fn list_tickets(conn: &Connection, filter: &ListFilter) -> Result<Vec<Ticket | |||
| 247 | 247 | } | |
| 248 | 248 | ||
| 249 | 249 | /// Update a ticket's status. Sets resolved_at when moving to Resolved. | |
| 250 | - | pub fn update_status(conn: &Connection, id: &str, status: Status) -> Result<()> { | |
| 250 | + | pub(crate) fn update_status(conn: &Connection, id: &str, status: Status) -> Result<()> { | |
| 251 | 251 | let now = Utc::now().to_rfc3339(); | |
| 252 | 252 | let resolved_at = if status == Status::Resolved { | |
| 253 | 253 | Some(now.clone()) | |
| @@ -267,7 +267,12 @@ pub fn update_status(conn: &Connection, id: &str, status: Status) -> Result<()> | |||
| 267 | 267 | } | |
| 268 | 268 | ||
| 269 | 269 | /// Update a ticket's title and body. Bumps `updated_at`. | |
| 270 | - | pub fn update_ticket(conn: &Connection, id: &str, title: &str, body: Option<&str>) -> Result<()> { | |
| 270 | + | pub(crate) fn update_ticket( | |
| 271 | + | conn: &Connection, | |
| 272 | + | id: &str, | |
| 273 | + | title: &str, | |
| 274 | + | body: Option<&str>, | |
| 275 | + | ) -> Result<()> { | |
| 271 | 276 | let now = Utc::now().to_rfc3339(); | |
| 272 | 277 | let rows = conn.execute( | |
| 273 | 278 | "UPDATE tickets SET title = ?1, body = ?2, updated_at = ?3 WHERE id = ?4", | |
| @@ -281,7 +286,7 @@ pub fn update_ticket(conn: &Connection, id: &str, title: &str, body: Option<&str | |||
| 281 | 286 | ||
| 282 | 287 | /// Aggregate stats across all tickets. | |
| 283 | 288 | #[derive(Debug, Default)] | |
| 284 | - | pub struct Stats { | |
| 289 | + | pub(crate) struct Stats { | |
| 285 | 290 | pub total: usize, | |
| 286 | 291 | pub by_status: Vec<(Status, usize)>, | |
| 287 | 292 | /// Open tickets bucketed by painhours color band. | |
| @@ -292,7 +297,7 @@ pub struct Stats { | |||
| 292 | 297 | pub avg_resolution_seconds: Option<i64>, | |
| 293 | 298 | } | |
| 294 | 299 | ||
| 295 | - | pub fn stats(conn: &Connection) -> Result<Stats> { | |
| 300 | + | pub(crate) fn stats(conn: &Connection) -> Result<Stats> { | |
| 296 | 301 | let tickets = list_tickets(conn, &ListFilter::default())?; | |
| 297 | 302 | let mut s = Stats { | |
| 298 | 303 | total: tickets.len(), | |
| @@ -354,7 +359,7 @@ pub fn stats(conn: &Connection) -> Result<Stats> { | |||
| 354 | 359 | ||
| 355 | 360 | /// Delete tickets older than `older_than` with the given status. | |
| 356 | 361 | /// Returns the number of rows deleted. | |
| 357 | - | pub fn prune_tickets( | |
| 362 | + | pub(crate) fn prune_tickets( | |
| 358 | 363 | conn: &Connection, | |
| 359 | 364 | older_than: chrono::Duration, | |
| 360 | 365 | status: Status, | |
| @@ -370,7 +375,7 @@ pub fn prune_tickets( | |||
| 370 | 375 | // -- Sync operations ---------------------------------------------------------- | |
| 371 | 376 | ||
| 372 | 377 | /// Get all tickets updated after the given timestamp. | |
| 373 | - | pub fn tickets_since(conn: &Connection, since: &str) -> Result<Vec<Ticket>> { | |
| 378 | + | pub(crate) fn tickets_since(conn: &Connection, since: &str) -> Result<Vec<Ticket>> { | |
| 374 | 379 | let mut stmt = | |
| 375 | 380 | conn.prepare("SELECT * FROM tickets WHERE updated_at > ?1 ORDER BY updated_at ASC")?; | |
| 376 | 381 | let tickets = stmt | |
| @@ -381,7 +386,7 @@ pub fn tickets_since(conn: &Connection, since: &str) -> Result<Vec<Ticket>> { | |||
| 381 | 386 | ||
| 382 | 387 | /// Upsert a ticket from a peer. Last-writer-wins based on updated_at. | |
| 383 | 388 | /// Returns true if the ticket was inserted or updated. | |
| 384 | - | pub fn upsert_synced_ticket(conn: &Connection, ticket: &Ticket) -> Result<bool> { | |
| 389 | + | pub(crate) fn upsert_synced_ticket(conn: &Connection, ticket: &Ticket) -> Result<bool> { | |
| 385 | 390 | // Check if we have this ticket and if ours is newer | |
| 386 | 391 | let existing_updated: Option<String> = conn | |
| 387 | 392 | .query_row( | |
| @@ -393,8 +398,7 @@ pub fn upsert_synced_ticket(conn: &Connection, ticket: &Ticket) -> Result<bool> | |||
| 393 | 398 | ||
| 394 | 399 | if let Some(ref existing) = existing_updated { | |
| 395 | 400 | let existing_dt = DateTime::parse_from_rfc3339(existing) | |
| 396 | - | .map(|dt| dt.with_timezone(&Utc)) | |
| 397 | - | .unwrap_or_else(|_| Utc::now()); | |
| 401 | + | .map_or_else(|_| Utc::now(), |dt| dt.with_timezone(&Utc)); | |
| 398 | 402 | if existing_dt >= ticket.updated_at { | |
| 399 | 403 | return Ok(false); // Ours is same or newer | |
| 400 | 404 | } | |
| @@ -437,7 +441,7 @@ pub fn upsert_synced_ticket(conn: &Connection, ticket: &Ticket) -> Result<bool> | |||
| 437 | 441 | } | |
| 438 | 442 | ||
| 439 | 443 | /// Get the sync cursor for a peer (last synced timestamp). | |
| 440 | - | pub fn get_sync_cursor(conn: &Connection, peer_url: &str) -> Result<Option<String>> { | |
| 444 | + | pub(crate) fn get_sync_cursor(conn: &Connection, peer_url: &str) -> Result<Option<String>> { | |
| 441 | 445 | let cursor: Option<String> = conn | |
| 442 | 446 | .query_row( | |
| 443 | 447 | "SELECT last_synced FROM sync_cursors WHERE peer_url = ?1", | |
| @@ -449,7 +453,7 @@ pub fn get_sync_cursor(conn: &Connection, peer_url: &str) -> Result<Option<Strin | |||
| 449 | 453 | } | |
| 450 | 454 | ||
| 451 | 455 | /// Update the sync cursor for a peer. | |
| 452 | - | pub fn set_sync_cursor(conn: &Connection, peer_url: &str, last_synced: &str) -> Result<()> { | |
| 456 | + | pub(crate) fn set_sync_cursor(conn: &Connection, peer_url: &str, last_synced: &str) -> Result<()> { | |
| 453 | 457 | conn.execute( | |
| 454 | 458 | "INSERT INTO sync_cursors (peer_url, last_synced) VALUES (?1, ?2) | |
| 455 | 459 | ON CONFLICT(peer_url) DO UPDATE SET last_synced = excluded.last_synced", |
| @@ -244,7 +244,7 @@ impl App { | |||
| 244 | 244 | ||
| 245 | 245 | // -- Entry point -------------------------------------------------------------- | |
| 246 | 246 | ||
| 247 | - | pub fn run(conn: Connection, node_id: String) -> Result<()> { | |
| 247 | + | pub(crate) fn run(conn: Connection, node_id: String) -> Result<()> { | |
| 248 | 248 | let mut terminal = ratatui::init(); | |
| 249 | 249 | let mut app = App::new(conn, node_id)?; | |
| 250 | 250 | ||
| @@ -526,12 +526,9 @@ fn render_list(app: &mut App, f: &mut Frame, area: Rect) { | |||
| 526 | 526 | } | |
| 527 | 527 | ||
| 528 | 528 | fn render_detail(app: &App, f: &mut Frame, area: Rect) { | |
| 529 | - | let ticket = match app.selected_ticket() { | |
| 530 | - | Some(t) => t, | |
| 531 | - | None => { | |
| 532 | - | f.render_widget(Paragraph::new("No ticket selected"), area); | |
| 533 | - | return; | |
| 534 | - | } | |
| 529 | + | let Some(ticket) = app.selected_ticket() else { | |
| 530 | + | f.render_widget(Paragraph::new("No ticket selected"), area); | |
| 531 | + | return; | |
| 535 | 532 | }; | |
| 536 | 533 | ||
| 537 | 534 | let mut lines = vec![ | |
| @@ -641,9 +638,8 @@ fn render_create_popup(app: &App, f: &mut Frame, area: Rect) { | |||
| 641 | 638 | } | |
| 642 | 639 | ||
| 643 | 640 | fn render_edit_popup(app: &App, f: &mut Frame, area: Rect) { | |
| 644 | - | let buf = match app.edit_buf.as_ref() { | |
| 645 | - | Some(b) => b, | |
| 646 | - | None => return, | |
| 641 | + | let Some(buf) = app.edit_buf.as_ref() else { | |
| 642 | + | return; | |
| 647 | 643 | }; | |
| 648 | 644 | ||
| 649 | 645 | let popup_width = 70.min(area.width.saturating_sub(4)); |
| @@ -59,13 +59,13 @@ const BAND_HIGH: u32 = 50; | |||
| 59 | 59 | const BAND_MEDIUM: u32 = 20; | |
| 60 | 60 | ||
| 61 | 61 | /// Default 1-5 guesstimate for both `pain` and `scale` when unspecified. | |
| 62 | - | pub const DEFAULT_FACTOR: u8 = 3; | |
| 62 | + | pub(crate) const DEFAULT_FACTOR: u8 = 3; | |
| 63 | 63 | ||
| 64 | 64 | // -- Priority (color band) ---------------------------------------------------- | |
| 65 | 65 | ||
| 66 | 66 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] | |
| 67 | 67 | #[serde(rename_all = "lowercase")] | |
| 68 | - | pub enum Priority { | |
| 68 | + | pub(crate) enum Priority { | |
| 69 | 69 | Low, | |
| 70 | 70 | Medium, | |
| 71 | 71 | High, | |
| @@ -73,7 +73,7 @@ pub enum Priority { | |||
| 73 | 73 | } | |
| 74 | 74 | ||
| 75 | 75 | impl Priority { | |
| 76 | - | pub fn color(self) -> Color { | |
| 76 | + | pub(crate) fn color(self) -> Color { | |
| 77 | 77 | match self { | |
| 78 | 78 | Self::Low => Color::DarkGray, | |
| 79 | 79 | Self::Medium => Color::White, | |
| @@ -83,7 +83,7 @@ impl Priority { | |||
| 83 | 83 | } | |
| 84 | 84 | ||
| 85 | 85 | /// Bucket a 0-100 painhours score into a color band. | |
| 86 | - | pub fn from_painhours(score: u32) -> Self { | |
| 86 | + | pub(crate) fn from_painhours(score: u32) -> Self { | |
| 87 | 87 | if score >= BAND_CRITICAL { | |
| 88 | 88 | Self::Critical | |
| 89 | 89 | } else if score >= BAND_HIGH { | |
| @@ -124,7 +124,7 @@ impl FromStr for Priority { | |||
| 124 | 124 | ||
| 125 | 125 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] | |
| 126 | 126 | #[serde(rename_all = "lowercase")] | |
| 127 | - | pub enum Status { | |
| 127 | + | pub(crate) enum Status { | |
| 128 | 128 | Open, | |
| 129 | 129 | InProgress, | |
| 130 | 130 | Resolved, | |
| @@ -132,7 +132,7 @@ pub enum Status { | |||
| 132 | 132 | } | |
| 133 | 133 | ||
| 134 | 134 | impl Status { | |
| 135 | - | pub fn indicator(self) -> char { | |
| 135 | + | pub(crate) fn indicator(self) -> char { | |
| 136 | 136 | match self { | |
| 137 | 137 | Self::Open => '*', | |
| 138 | 138 | Self::InProgress => '>', | |
| @@ -171,7 +171,7 @@ impl FromStr for Status { | |||
| 171 | 171 | /// Ticket channel: who is communicating with whom. | |
| 172 | 172 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] | |
| 173 | 173 | #[serde(rename_all = "lowercase")] | |
| 174 | - | pub enum Channel { | |
| 174 | + | pub(crate) enum Channel { | |
| 175 | 175 | /// System-to-system: automated alerts, CI results, health checks. | |
| 176 | 176 | /// No human response expected. | |
| 177 | 177 | System, | |
| @@ -207,7 +207,7 @@ impl FromStr for Channel { | |||
| 207 | 207 | // -- Ticket ------------------------------------------------------------------- | |
| 208 | 208 | ||
| 209 | 209 | #[derive(Debug, Clone, Serialize, Deserialize)] | |
| 210 | - | pub struct Ticket { | |
| 210 | + | pub(crate) struct Ticket { | |
| 211 | 211 | pub id: String, | |
| 212 | 212 | pub title: String, | |
| 213 | 213 | pub body: Option<String>, | |
| @@ -227,7 +227,7 @@ pub struct Ticket { | |||
| 227 | 227 | ||
| 228 | 228 | impl Ticket { | |
| 229 | 229 | /// Human-readable age string (e.g. "3m", "2h", "5d"). | |
| 230 | - | pub fn age(&self) -> String { | |
| 230 | + | pub(crate) fn age(&self) -> String { | |
| 231 | 231 | let dur = Utc::now() - self.created_at; | |
| 232 | 232 | if dur.num_days() > 0 { | |
| 233 | 233 | format!("{}d", dur.num_days()) | |
| @@ -249,7 +249,7 @@ impl Ticket { | |||
| 249 | 249 | ||
| 250 | 250 | /// Age in whole weeks, rounded up (overestimating is fine), floored at 1 so | |
| 251 | 251 | /// a brand-new ticket still contributes to its score. | |
| 252 | - | pub fn age_weeks(&self) -> u32 { | |
| 252 | + | pub(crate) fn age_weeks(&self) -> u32 { | |
| 253 | 253 | let secs = (self.age_anchor() - self.created_at).num_seconds().max(0); | |
| 254 | 254 | let weeks = (secs as f64 / (7.0 * 86_400.0)).ceil(); | |
| 255 | 255 | (weeks as u32).max(1) | |
| @@ -258,7 +258,7 @@ impl Ticket { | |||
| 258 | 258 | /// The painhours score: a 0-100 urgency number operators sort by. | |
| 259 | 259 | /// `round(100 * (1 - e^(-raw/K)))` where | |
| 260 | 260 | /// `raw = pain^PAIN_EXP * scale^SCALE_EXP * age_weeks^AGE_EXP`. | |
| 261 | - | pub fn painhours(&self) -> u32 { | |
| 261 | + | pub(crate) fn painhours(&self) -> u32 { | |
| 262 | 262 | let pain = (self.pain.clamp(1, 5) as f64).powf(PAIN_EXP); | |
| 263 | 263 | let scale = (self.scale.clamp(1, 5) as f64).powf(SCALE_EXP); | |
| 264 | 264 | let age = (self.age_weeks() as f64).powf(AGE_EXP); | |
| @@ -267,17 +267,17 @@ impl Ticket { | |||
| 267 | 267 | } | |
| 268 | 268 | ||
| 269 | 269 | /// Color band derived from the painhours score. | |
| 270 | - | pub fn band(&self) -> Priority { | |
| 270 | + | pub(crate) fn band(&self) -> Priority { | |
| 271 | 271 | Priority::from_painhours(self.painhours()) | |
| 272 | 272 | } | |
| 273 | 273 | ||
| 274 | 274 | /// Short ID prefix for display. | |
| 275 | - | pub fn short_id(&self) -> &str { | |
| 275 | + | pub(crate) fn short_id(&self) -> &str { | |
| 276 | 276 | &self.id[..8.min(self.id.len())] | |
| 277 | 277 | } | |
| 278 | 278 | ||
| 279 | 279 | /// Short node ID for display. | |
| 280 | - | pub fn short_node(&self) -> &str { | |
| 280 | + | pub(crate) fn short_node(&self) -> &str { | |
| 281 | 281 | &self.node_id[..8.min(self.node_id.len())] | |
| 282 | 282 | } | |
| 283 | 283 | } | |
| @@ -285,7 +285,7 @@ impl Ticket { | |||
| 285 | 285 | // -- NewTicket (for create) --------------------------------------------------- | |
| 286 | 286 | ||
| 287 | 287 | #[derive(Debug, Serialize, Deserialize)] | |
| 288 | - | pub struct NewTicket { | |
| 288 | + | pub(crate) struct NewTicket { | |
| 289 | 289 | pub title: String, | |
| 290 | 290 | #[serde(default)] | |
| 291 | 291 | pub body: Option<String>, |