max / makenotwork
23 files changed,
+76 insertions,
-97 deletions
| @@ -254,6 +254,7 @@ pub struct TagInfo { | |||
| 254 | 254 | ||
| 255 | 255 | /// Result of a broadcast send. | |
| 256 | 256 | #[derive(Debug, Deserialize)] | |
| 257 | + | #[allow(dead_code)] // wire contract: every field the endpoint returns, read or not | |
| 257 | 258 | pub struct BroadcastResult { | |
| 258 | 259 | pub success: bool, | |
| 259 | 260 | pub recipient_count: usize, | |
| @@ -292,6 +293,7 @@ pub struct DomainInfo { | |||
| 292 | 293 | ||
| 293 | 294 | /// Domain verification result. | |
| 294 | 295 | #[derive(Debug, Deserialize)] | |
| 296 | + | #[allow(dead_code)] // wire contract: every field the endpoint returns, read or not | |
| 295 | 297 | pub struct DomainVerifyResult { | |
| 296 | 298 | pub verified: bool, | |
| 297 | 299 | pub message: String, | |
| @@ -1371,6 +1373,9 @@ impl MnwApiClient { | |||
| 1371 | 1373 | empty_response(resp, "add_item_tag").await | |
| 1372 | 1374 | } | |
| 1373 | 1375 | ||
| 1376 | + | // Unused by the TUI today; kept so the client mirrors the full | |
| 1377 | + | // /api/internal surface rather than only the paths one caller happens to hit. | |
| 1378 | + | #[allow(dead_code)] | |
| 1374 | 1379 | pub async fn remove_item_tag(&self, user_id: &str, item_id: &str, tag_id: &str) -> anyhow::Result<()> { | |
| 1375 | 1380 | let url = format!("{}/api/internal/creator/items/tags/remove", self.base_url); | |
| 1376 | 1381 | let resp = self.http.post(&url).bearer_auth(&self.service_token) | |
| @@ -1411,6 +1416,7 @@ impl MnwApiClient { | |||
| 1411 | 1416 | json_response(resp, "list_collections").await | |
| 1412 | 1417 | } | |
| 1413 | 1418 | ||
| 1419 | + | #[allow(dead_code)] | |
| 1414 | 1420 | pub async fn create_collection(&self, user_id: &str, slug: &str, title: &str) -> anyhow::Result<serde_json::Value> { | |
| 1415 | 1421 | let url = format!("{}/api/internal/creator/collections", self.base_url); | |
| 1416 | 1422 | let resp = self.http.post(&url).bearer_auth(&self.service_token) | |
| @@ -1420,6 +1426,7 @@ impl MnwApiClient { | |||
| 1420 | 1426 | json_response(resp, "create_collection").await | |
| 1421 | 1427 | } | |
| 1422 | 1428 | ||
| 1429 | + | #[allow(dead_code)] | |
| 1423 | 1430 | pub async fn delete_collection(&self, user_id: &str, collection_id: &str) -> anyhow::Result<()> { | |
| 1424 | 1431 | let url = format!("{}/api/internal/creator/collections/{}", self.base_url, collection_id); | |
| 1425 | 1432 | let resp = self.http.delete(&url).bearer_auth(&self.service_token) |
| @@ -53,7 +53,7 @@ pub async fn execute( | |||
| 53 | 53 | _ => b"Usage: project create --title \"Name\" [--type audio|digital|video|mixed|subscription] [--description \"...\"]\r\n".to_vec(), | |
| 54 | 54 | }, | |
| 55 | 55 | "upload" => { | |
| 56 | - | return b"Pipe uploads use stdin. Example:\r\n cat file.wav | ssh cli.makenot.work upload --filename track.wav --project my-project\r\n".to_vec(); | |
| 56 | + | b"Pipe uploads use stdin. Example:\r\n cat file.wav | ssh cli.makenot.work upload --filename track.wav --project my-project\r\n".to_vec() | |
| 57 | 57 | } | |
| 58 | 58 | "projects" => cmd_projects(user, api, json).await, | |
| 59 | 59 | "analytics" => { | |
| @@ -403,11 +403,10 @@ async fn cmd_domain_show(user: &UserInfo, api: &MnwApiClient) -> Vec<u8> { | |||
| 403 | 403 | Ok(Some(d)) => { | |
| 404 | 404 | let status = if d.verified { "verified" } else { "pending" }; | |
| 405 | 405 | let mut out = format!("Domain: {} ({})\r\n", d.domain, status); | |
| 406 | - | if !d.verified { | |
| 407 | - | if let Some(ref instr) = d.instructions { | |
| 406 | + | if !d.verified | |
| 407 | + | && let Some(ref instr) = d.instructions { | |
| 408 | 408 | out.push_str(&format!("{}\r\n", instr)); | |
| 409 | 409 | } | |
| 410 | - | } | |
| 411 | 410 | out.into_bytes() | |
| 412 | 411 | } | |
| 413 | 412 | Ok(None) => b"No custom domain configured.\r\nUsage: domain add <domain>\r\n".to_vec(), | |
| @@ -557,8 +556,8 @@ pub fn help_text() -> Vec<u8> { | |||
| 557 | 556 | /// Handles quoted values that were split by whitespace by rejoining until the closing quote. | |
| 558 | 557 | fn extract_flag(parts: &[&str], flags: &[&str]) -> Option<String> { | |
| 559 | 558 | for (i, part) in parts.iter().enumerate() { | |
| 560 | - | if flags.contains(part) { | |
| 561 | - | if let Some(&next) = parts.get(i + 1) { | |
| 559 | + | if flags.contains(part) | |
| 560 | + | && let Some(&next) = parts.get(i + 1) { | |
| 562 | 561 | // If the value starts with a quote, collect until closing quote | |
| 563 | 562 | if next.starts_with('"') || next.starts_with('\'') { | |
| 564 | 563 | let quote = next.as_bytes()[0] as char; | |
| @@ -579,7 +578,6 @@ fn extract_flag(parts: &[&str], flags: &[&str]) -> Option<String> { | |||
| 579 | 578 | } | |
| 580 | 579 | return Some(next.to_string()); | |
| 581 | 580 | } | |
| 582 | - | } | |
| 583 | 581 | } | |
| 584 | 582 | None | |
| 585 | 583 | } |
| @@ -86,15 +86,14 @@ impl russh::server::Handler for MnwHandler { | |||
| 86 | 86 | key: &PublicKey, | |
| 87 | 87 | ) -> Result<Auth, Self::Error> { | |
| 88 | 88 | // Per-IP rate limiting: reject early if threshold exceeded | |
| 89 | - | if let Some(addr) = self.peer_addr { | |
| 90 | - | if !self.rate_limiter.check(addr.ip()) { | |
| 89 | + | if let Some(addr) = self.peer_addr | |
| 90 | + | && !self.rate_limiter.check(addr.ip()) { | |
| 91 | 91 | tracing::warn!(peer = %addr, "auth rate limit exceeded"); | |
| 92 | 92 | return Ok(Auth::Reject { | |
| 93 | 93 | proceed_with_methods: None, | |
| 94 | 94 | partial_success: false, | |
| 95 | 95 | }); | |
| 96 | 96 | } | |
| 97 | - | } | |
| 98 | 97 | ||
| 99 | 98 | let fingerprint = key.fingerprint(HashAlg::Sha256).to_string(); | |
| 100 | 99 | tracing::debug!(%fingerprint, peer = ?self.peer_addr, "key offered"); | |
| @@ -424,10 +423,10 @@ impl russh::server::Handler for MnwHandler { | |||
| 424 | 423 | let mut i = 1; | |
| 425 | 424 | while i < parts.len() { | |
| 426 | 425 | match parts[i] { | |
| 427 | - | "--filename" | "-f" => { if i + 1 < parts.len() { filename = parts[i + 1].to_string(); i += 1; } } | |
| 428 | - | "--project" | "-p" => { if i + 1 < parts.len() { project_slug = parts[i + 1].to_string(); i += 1; } } | |
| 429 | - | "--title" | "-t" => { if i + 1 < parts.len() { title = parts[i + 1].to_string(); i += 1; } } | |
| 430 | - | "--price" => { if i + 1 < parts.len() { price_cents = parts[i + 1].parse().unwrap_or(0); i += 1; } } | |
| 426 | + | "--filename" | "-f" if i + 1 < parts.len() => { filename = parts[i + 1].to_string(); i += 1; } | |
| 427 | + | "--project" | "-p" if i + 1 < parts.len() => { project_slug = parts[i + 1].to_string(); i += 1; } | |
| 428 | + | "--title" | "-t" if i + 1 < parts.len() => { title = parts[i + 1].to_string(); i += 1; } | |
| 429 | + | "--price" if i + 1 < parts.len() => { price_cents = parts[i + 1].parse().unwrap_or(0); i += 1; } | |
| 431 | 430 | _ => {} | |
| 432 | 431 | } | |
| 433 | 432 | i += 1; |
| @@ -60,7 +60,7 @@ pub async fn list_staged_files(dir: &Path) -> Vec<StagedFile> { | |||
| 60 | 60 | }); | |
| 61 | 61 | } | |
| 62 | 62 | ||
| 63 | - | files.sort_by(|a, b| b.modified.cmp(&a.modified)); | |
| 63 | + | files.sort_by_key(|f| std::cmp::Reverse(f.modified)); | |
| 64 | 64 | files | |
| 65 | 65 | } | |
| 66 | 66 |
| @@ -10,16 +10,14 @@ pub(crate) async fn handle_analytics_input( | |||
| 10 | 10 | tx: &mpsc::Sender<AppEvent>, | |
| 11 | 11 | ) { | |
| 12 | 12 | match key.code { | |
| 13 | - | KeyCode::Char('j') | KeyCode::Down => { | |
| 14 | - | if app.analytics_show_transactions { | |
| 13 | + | KeyCode::Char('j') | KeyCode::Down | |
| 14 | + | if app.analytics_show_transactions => { | |
| 15 | 15 | app.move_down(screen); | |
| 16 | 16 | } | |
| 17 | - | } | |
| 18 | - | KeyCode::Char('k') | KeyCode::Up => { | |
| 19 | - | if app.analytics_show_transactions { | |
| 17 | + | KeyCode::Char('k') | KeyCode::Up | |
| 18 | + | if app.analytics_show_transactions => { | |
| 20 | 19 | app.move_up(screen); | |
| 21 | 20 | } | |
| 22 | - | } | |
| 23 | 21 | KeyCode::Esc => { | |
| 24 | 22 | if app.analytics_show_transactions { | |
| 25 | 23 | app.analytics_show_transactions = false; |
| @@ -138,8 +138,8 @@ pub(crate) async fn handle_blog_input( | |||
| 138 | 138 | app.edit_buffer.clear(); | |
| 139 | 139 | app.blog_status = Some("Title: _".to_string()); | |
| 140 | 140 | } | |
| 141 | - | KeyCode::Char('d') | KeyCode::Char('D') => { | |
| 142 | - | if !app.blog_posts.is_empty() { | |
| 141 | + | KeyCode::Char('d') | KeyCode::Char('D') | |
| 142 | + | if !app.blog_posts.is_empty() => { | |
| 143 | 143 | let idx = app.selected_index; | |
| 144 | 144 | if matches!(app.confirm_action, Some(ConfirmAction::DeleteBlogPost { post_idx }) if post_idx == idx) { | |
| 145 | 145 | // Confirmed — execute delete | |
| @@ -183,7 +183,6 @@ pub(crate) async fn handle_blog_input( | |||
| 183 | 183 | )); | |
| 184 | 184 | } | |
| 185 | 185 | } | |
| 186 | - | } | |
| 187 | 186 | KeyCode::Char('r') | KeyCode::Char('R') => { | |
| 188 | 187 | if let Screen::Blog(_, project_id) = &*screen { | |
| 189 | 188 | app.loading = true; |
| @@ -13,8 +13,8 @@ pub(crate) async fn handle_home_input( | |||
| 13 | 13 | match key.code { | |
| 14 | 14 | KeyCode::Char('j') | KeyCode::Down => app.move_down(screen), | |
| 15 | 15 | KeyCode::Char('k') | KeyCode::Up => app.move_up(screen), | |
| 16 | - | KeyCode::Enter => { | |
| 17 | - | if !app.projects.is_empty() { | |
| 16 | + | KeyCode::Enter | |
| 17 | + | if !app.projects.is_empty() => { | |
| 18 | 18 | let idx = app.selected_index; | |
| 19 | 19 | let project_id = app.projects[idx].id.clone(); | |
| 20 | 20 | let user_id = app.user.user_id.clone(); | |
| @@ -29,7 +29,6 @@ pub(crate) async fn handle_home_input( | |||
| 29 | 29 | load_project_items(&api, &project_id, &user_id, &tx).await; | |
| 30 | 30 | }); | |
| 31 | 31 | } | |
| 32 | - | } | |
| 33 | 32 | KeyCode::Char('u') | KeyCode::Char('U') => { | |
| 34 | 33 | *screen = Screen::Upload; | |
| 35 | 34 | app.selected_index = 0; |
| @@ -365,14 +365,13 @@ pub(crate) async fn handle_item_input( | |||
| 365 | 365 | } | |
| 366 | 366 | } | |
| 367 | 367 | } | |
| 368 | - | KeyCode::Char('t') | KeyCode::Char('T') => { | |
| 369 | - | if app.item_detail.is_some() { | |
| 368 | + | KeyCode::Char('t') | KeyCode::Char('T') | |
| 369 | + | if app.item_detail.is_some() => { | |
| 370 | 370 | app.tag_searching = true; | |
| 371 | 371 | app.edit_buffer.clear(); | |
| 372 | 372 | app.tag_search_results.clear(); | |
| 373 | 373 | app.item_status = Some("Tag: type to search, Enter to add first result, Esc to cancel".to_string()); | |
| 374 | 374 | } | |
| 375 | - | } | |
| 376 | 375 | KeyCode::Char('l') | KeyCode::Char('L') => { | |
| 377 | 376 | // Open license keys screen | |
| 378 | 377 | if let Screen::Item(project_idx, item_id) = &*screen { |
| @@ -65,8 +65,8 @@ pub(crate) async fn handle_keys_input( | |||
| 65 | 65 | }); | |
| 66 | 66 | } | |
| 67 | 67 | } | |
| 68 | - | KeyCode::Char('x') | KeyCode::Char('X') => { | |
| 69 | - | if !app.license_keys.is_empty() { | |
| 68 | + | KeyCode::Char('x') | KeyCode::Char('X') | |
| 69 | + | if !app.license_keys.is_empty() => { | |
| 70 | 70 | let idx = app.selected_index; | |
| 71 | 71 | if matches!(app.confirm_action, Some(ConfirmAction::RevokeLicenseKey { key_idx }) if key_idx == idx) { | |
| 72 | 72 | // Confirmed — execute revoke | |
| @@ -110,7 +110,6 @@ pub(crate) async fn handle_keys_input( | |||
| 110 | 110 | )); | |
| 111 | 111 | } | |
| 112 | 112 | } | |
| 113 | - | } | |
| 114 | 113 | KeyCode::Char('r') | KeyCode::Char('R') => { | |
| 115 | 114 | if let Screen::Keys(_, item_id) = &*screen { | |
| 116 | 115 | app.loading = true; |
| @@ -44,9 +44,9 @@ pub(crate) async fn handle_project_input( | |||
| 44 | 44 | match key.code { | |
| 45 | 45 | KeyCode::Char('j') | KeyCode::Down => app.move_down(screen), | |
| 46 | 46 | KeyCode::Char('k') | KeyCode::Up => app.move_up(screen), | |
| 47 | - | KeyCode::Char(' ') => { | |
| 47 | + | KeyCode::Char(' ') | |
| 48 | 48 | // Toggle selection | |
| 49 | - | if !app.items.is_empty() { | |
| 49 | + | if !app.items.is_empty() => { | |
| 50 | 50 | let idx = app.selected_index; | |
| 51 | 51 | if app.selected_items.contains(&idx) { | |
| 52 | 52 | app.selected_items.remove(&idx); | |
| @@ -54,7 +54,6 @@ pub(crate) async fn handle_project_input( | |||
| 54 | 54 | app.selected_items.insert(idx); | |
| 55 | 55 | } | |
| 56 | 56 | } | |
| 57 | - | } | |
| 58 | 57 | KeyCode::Esc => { | |
| 59 | 58 | if !app.selected_items.is_empty() { | |
| 60 | 59 | app.selected_items.clear(); | |
| @@ -87,8 +86,8 @@ pub(crate) async fn handle_project_input( | |||
| 87 | 86 | }); | |
| 88 | 87 | } | |
| 89 | 88 | } | |
| 90 | - | KeyCode::Char('p') | KeyCode::Char('P') => { | |
| 91 | - | if !app.items.is_empty() { | |
| 89 | + | KeyCode::Char('p') | KeyCode::Char('P') | |
| 90 | + | if !app.items.is_empty() => { | |
| 92 | 91 | if app.selected_items.is_empty() { | |
| 93 | 92 | // Single item: toggle publish state | |
| 94 | 93 | let item = &app.items[app.selected_index]; | |
| @@ -122,9 +121,8 @@ pub(crate) async fn handle_project_input( | |||
| 122 | 121 | } | |
| 123 | 122 | } | |
| 124 | 123 | } | |
| 125 | - | } | |
| 126 | - | KeyCode::Char('d') | KeyCode::Char('D') => { | |
| 127 | - | if !app.items.is_empty() { | |
| 124 | + | KeyCode::Char('d') | KeyCode::Char('D') | |
| 125 | + | if !app.items.is_empty() => { | |
| 128 | 126 | if app.selected_items.is_empty() { | |
| 129 | 127 | // Select current item for single delete | |
| 130 | 128 | app.selected_items.insert(app.selected_index); | |
| @@ -132,7 +130,6 @@ pub(crate) async fn handle_project_input( | |||
| 132 | 130 | let count = app.selected_items.len(); | |
| 133 | 131 | app.confirm_action = Some(ConfirmAction::BulkDelete { count }); | |
| 134 | 132 | } | |
| 135 | - | } | |
| 136 | 133 | KeyCode::Char('b') | KeyCode::Char('B') => { | |
| 137 | 134 | if let Screen::Project(idx) = screen | |
| 138 | 135 | && let Some(p) = app.projects.get(*idx) |
| @@ -100,8 +100,8 @@ pub(crate) async fn handle_promo_input( | |||
| 100 | 100 | app.edit_buffer.clear(); | |
| 101 | 101 | app.promo_status = Some("Code: _".to_string()); | |
| 102 | 102 | } | |
| 103 | - | KeyCode::Char('d') | KeyCode::Char('D') => { | |
| 104 | - | if !app.promo_codes.is_empty() { | |
| 103 | + | KeyCode::Char('d') | KeyCode::Char('D') | |
| 104 | + | if !app.promo_codes.is_empty() => { | |
| 105 | 105 | let idx = app.selected_index; | |
| 106 | 106 | if matches!(app.confirm_action, Some(ConfirmAction::DeletePromoCode { code_idx }) if code_idx == idx) { | |
| 107 | 107 | // Confirmed — execute delete | |
| @@ -141,7 +141,6 @@ pub(crate) async fn handle_promo_input( | |||
| 141 | 141 | )); | |
| 142 | 142 | } | |
| 143 | 143 | } | |
| 144 | - | } | |
| 145 | 144 | KeyCode::Char('r') | KeyCode::Char('R') => { | |
| 146 | 145 | app.loading = true; | |
| 147 | 146 | let api = api.clone(); |
| @@ -102,11 +102,10 @@ pub(crate) async fn handle_upload_input( | |||
| 102 | 102 | } | |
| 103 | 103 | ||
| 104 | 104 | // Cancel pending delete confirmation on non-d keys | |
| 105 | - | if !matches!(key.code, KeyCode::Char('d') | KeyCode::Char('D')) { | |
| 106 | - | if app.upload_status.as_ref().is_some_and(|s| s.starts_with("Delete '") && s.ends_with("'? Press d again")) { | |
| 105 | + | if !matches!(key.code, KeyCode::Char('d') | KeyCode::Char('D')) | |
| 106 | + | && app.upload_status.as_ref().is_some_and(|s| s.starts_with("Delete '") && s.ends_with("'? Press d again")) { | |
| 107 | 107 | app.upload_status = None; | |
| 108 | 108 | } | |
| 109 | - | } | |
| 110 | 109 | ||
| 111 | 110 | // Normal mode | |
| 112 | 111 | match key.code { | |
| @@ -119,8 +118,8 @@ pub(crate) async fn handle_upload_input( | |||
| 119 | 118 | app.upload_status = None; | |
| 120 | 119 | app.selected_index = 0; | |
| 121 | 120 | } | |
| 122 | - | KeyCode::Char('e') | KeyCode::Char('E') => { | |
| 123 | - | if !app.staged_files.is_empty() { | |
| 121 | + | KeyCode::Char('e') | KeyCode::Char('E') | |
| 122 | + | if !app.staged_files.is_empty() => { | |
| 124 | 123 | let idx = app.selected_index; | |
| 125 | 124 | let current_title = app.file_metadata.get(idx).and_then(|m| m.title.clone()); | |
| 126 | 125 | app.editing_field = Some(EditField::Title); | |
| @@ -128,9 +127,8 @@ pub(crate) async fn handle_upload_input( | |||
| 128 | 127 | app.upload_status = | |
| 129 | 128 | Some("Title: _ (Enter to save, Tab for next field, Esc to cancel)".to_string()); | |
| 130 | 129 | } | |
| 131 | - | } | |
| 132 | - | KeyCode::Char('p') | KeyCode::Char('P') => { | |
| 133 | - | if !app.staged_files.is_empty() && !app.publishing { | |
| 130 | + | KeyCode::Char('p') | KeyCode::Char('P') | |
| 131 | + | if !app.staged_files.is_empty() && !app.publishing => { | |
| 134 | 132 | let idx = app.selected_index; | |
| 135 | 133 | let file = &app.staged_files[idx]; | |
| 136 | 134 | let meta = app.file_metadata.get(idx).cloned().unwrap_or_default(); | |
| @@ -198,9 +196,8 @@ pub(crate) async fn handle_upload_input( | |||
| 198 | 196 | .await; | |
| 199 | 197 | }); | |
| 200 | 198 | } | |
| 201 | - | } | |
| 202 | - | KeyCode::Char('d') | KeyCode::Char('D') => { | |
| 203 | - | if !app.staged_files.is_empty() { | |
| 199 | + | KeyCode::Char('d') | KeyCode::Char('D') | |
| 200 | + | if !app.staged_files.is_empty() => { | |
| 204 | 201 | let idx = app.selected_index; | |
| 205 | 202 | let filename = &app.staged_files[idx].filename; | |
| 206 | 203 | if app.upload_status.as_ref().is_some_and(|s| s.starts_with("Delete '") && s.ends_with("'? Press d again")) { | |
| @@ -223,7 +220,6 @@ pub(crate) async fn handle_upload_input( | |||
| 223 | 220 | app.upload_status = Some(format!("Delete '{}'? Press d again", filename)); | |
| 224 | 221 | } | |
| 225 | 222 | } | |
| 226 | - | } | |
| 227 | 223 | KeyCode::Char('r') | KeyCode::Char('R') => { | |
| 228 | 224 | app.loading = true; | |
| 229 | 225 | let staging_dir = staging_dir.to_path_buf(); |
| @@ -166,7 +166,9 @@ enum Screen { | |||
| 166 | 166 | /// Collections management. | |
| 167 | 167 | Collections, | |
| 168 | 168 | /// Subscription tiers for a project. Stores (project_index, project_id). | |
| 169 | - | Tiers(usize, String), | |
| 169 | + | /// The id is carried for the tier-mutation calls the screen will need; the | |
| 170 | + | /// render path only uses the index today. | |
| 171 | + | Tiers(usize, #[allow(dead_code)] String), | |
| 170 | 172 | } | |
| 171 | 173 | ||
| 172 | 174 | /// User-editable metadata for a staged file. | |
| @@ -505,7 +507,7 @@ mod tests { | |||
| 505 | 507 | ||
| 506 | 508 | #[test] | |
| 507 | 509 | fn parse_key_printable_char() { | |
| 508 | - | let key = parse_key(&[b'a']).unwrap(); | |
| 510 | + | let key = parse_key(b"a").unwrap(); | |
| 509 | 511 | assert_eq!(key.code, KeyCode::Char('a')); | |
| 510 | 512 | } | |
| 511 | 513 |
| @@ -309,8 +309,8 @@ pub fn launch( | |||
| 309 | 309 | app.item_status = Some(message); | |
| 310 | 310 | app.selected_items.clear(); | |
| 311 | 311 | // Reload project items | |
| 312 | - | if let Screen::Project(pidx) = &screen { | |
| 313 | - | if let Some(p) = app.projects.get(*pidx) { | |
| 312 | + | if let Screen::Project(pidx) = &screen | |
| 313 | + | && let Some(p) = app.projects.get(*pidx) { | |
| 314 | 314 | app.loading = true; | |
| 315 | 315 | let api = api.clone(); | |
| 316 | 316 | let project_id = p.id.clone(); | |
| @@ -320,7 +320,6 @@ pub fn launch( | |||
| 320 | 320 | load_project_items(&api, &project_id, &user_id, &tx).await; | |
| 321 | 321 | }); | |
| 322 | 322 | } | |
| 323 | - | } | |
| 324 | 323 | } | |
| 325 | 324 | DataPayload::ProjectReload { project_idx } => { | |
| 326 | 325 | if let Some(p) = app.projects.get(project_idx) { |
| @@ -505,9 +505,9 @@ dependencies = [ | |||
| 505 | 505 | ||
| 506 | 506 | [[package]] | |
| 507 | 507 | name = "crossbeam-epoch" | |
| 508 | - | version = "0.9.18" | |
| 508 | + | version = "0.9.20" | |
| 509 | 509 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 510 | - | checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" | |
| 510 | + | checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" | |
| 511 | 511 | dependencies = [ | |
| 512 | 512 | "crossbeam-utils", | |
| 513 | 513 | ] |
| @@ -368,9 +368,9 @@ checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" | |||
| 368 | 368 | ||
| 369 | 369 | [[package]] | |
| 370 | 370 | name = "crossbeam-epoch" | |
| 371 | - | version = "0.9.18" | |
| 371 | + | version = "0.9.20" | |
| 372 | 372 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 373 | - | checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" | |
| 373 | + | checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" | |
| 374 | 374 | dependencies = [ | |
| 375 | 375 | "crossbeam-utils", | |
| 376 | 376 | ] |
| @@ -2128,9 +2128,9 @@ dependencies = [ | |||
| 2128 | 2128 | ||
| 2129 | 2129 | [[package]] | |
| 2130 | 2130 | name = "crossbeam-epoch" | |
| 2131 | - | version = "0.9.18" | |
| 2131 | + | version = "0.9.20" | |
| 2132 | 2132 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2133 | - | checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" | |
| 2133 | + | checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" | |
| 2134 | 2134 | dependencies = [ | |
| 2135 | 2135 | "crossbeam-utils", | |
| 2136 | 2136 | ] |
| @@ -16,7 +16,7 @@ | |||
| 16 | 16 | -- `dedup_key` lets an agent collapse repeated firings of the same condition | |
| 17 | 17 | -- (e.g. one row per (source, dedup_key) flap) without the server modeling agent | |
| 18 | 18 | -- state; it is nullable for one-shot alerts. | |
| 19 | - | CREATE TABLE admin_alerts ( | |
| 19 | + | CREATE TABLE IF NOT EXISTS admin_alerts ( | |
| 20 | 20 | id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, | |
| 21 | 21 | source TEXT NOT NULL, | |
| 22 | 22 | kind TEXT NOT NULL, | |
| @@ -30,4 +30,4 @@ CREATE TABLE admin_alerts ( | |||
| 30 | 30 | ); | |
| 31 | 31 | ||
| 32 | 32 | -- The admin log view is "newest first", so index the sort key. | |
| 33 | - | CREATE INDEX idx_admin_alerts_received ON admin_alerts (received_at DESC); | |
| 33 | + | CREATE INDEX IF NOT EXISTS idx_admin_alerts_received ON admin_alerts (received_at DESC); |
| @@ -786,17 +786,6 @@ fn query_param_contract() -> Vec<(&'static str, bool)> { | |||
| 786 | 786 | ] | |
| 787 | 787 | } | |
| 788 | 788 | ||
| 789 | - | /// The names the rendered page must expose as form controls, for the | |
| 790 | - | /// integration test that checks the markup. | |
| 791 | - | #[cfg(test)] | |
| 792 | - | pub(crate) fn required_control_names() -> Vec<&'static str> { | |
| 793 | - | query_param_contract() | |
| 794 | - | .into_iter() | |
| 795 | - | .filter(|(_, required)| *required) | |
| 796 | - | .map(|(name, _)| name) | |
| 797 | - | .collect() | |
| 798 | - | } | |
| 799 | - | ||
| 800 | 789 | #[cfg(test)] | |
| 801 | 790 | mod query_contract_tests { | |
| 802 | 791 | use super::*; |
| @@ -723,10 +723,12 @@ async fn sidebar_uses_only_defined_style_hooks() { | |||
| 723 | 723 | /// | |
| 724 | 724 | /// A form's control names are an untested contract with its handler. A rename | |
| 725 | 725 | /// pass once rewrote `name="has_source"` to `name="sidebar.has_source"` and the | |
| 726 | - | /// whole suite passed with the filter inert. The list is derived from an | |
| 727 | - | /// exhaustive destructure of `DiscoverQuery` (see `query_param_contract`), so a | |
| 728 | - | /// new query parameter cannot be added without deciding whether it needs a | |
| 729 | - | /// control here. | |
| 726 | + | /// whole suite passed with the filter inert. | |
| 727 | + | /// | |
| 728 | + | /// The list below is hardcoded. `query_param_contract` in the handler is the | |
| 729 | + | /// exhaustive destructure of `DiscoverQuery`, but it lives behind two private | |
| 730 | + | /// module boundaries and this is a separate crate, so the two can drift: adding | |
| 731 | + | /// a query parameter there does NOT fail anything here. | |
| 730 | 732 | #[tokio::test] | |
| 731 | 733 | async fn every_filter_param_has_a_control_in_the_markup() { | |
| 732 | 734 | let mut h = TestHarness::new().await; |