Skip to main content

dakera_client/
admin.rs

1//! Admin operations for the Dakera client.
2//!
3//! Provides methods for cluster management, cache, configuration, quotas,
4//! slow queries, backups, and TTL management.
5
6use std::collections::HashMap;
7
8use serde::{Deserialize, Serialize};
9
10use crate::error::Result;
11use crate::DakeraClient;
12
13// ============================================================================
14// Cluster Types
15// ============================================================================
16
17/// Cluster status response
18#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct ClusterStatus {
20    pub cluster_id: String,
21    pub state: String,
22    pub node_count: u32,
23    pub total_vectors: u64,
24    pub namespace_count: u64,
25    pub version: String,
26    pub timestamp: u64,
27}
28
29/// Node information
30#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct NodeInfo {
32    pub node_id: String,
33    pub address: String,
34    pub role: String,
35    pub status: String,
36    pub version: String,
37    pub uptime_seconds: u64,
38    pub vector_count: u64,
39    pub memory_bytes: u64,
40    #[serde(default)]
41    pub cpu_percent: f32,
42    #[serde(default)]
43    pub memory_percent: f32,
44    pub last_heartbeat: u64,
45}
46
47/// Node list response
48#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct NodeListResponse {
50    pub nodes: Vec<NodeInfo>,
51    pub total: u32,
52}
53
54// ============================================================================
55// Namespace Admin Types
56// ============================================================================
57
58/// Index statistics
59#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct IndexStats {
61    pub index_type: String,
62    pub is_built: bool,
63    pub size_bytes: u64,
64    pub indexed_vectors: u64,
65    #[serde(skip_serializing_if = "Option::is_none")]
66    pub last_rebuild: Option<u64>,
67}
68
69/// Detailed namespace statistics
70#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct NamespaceAdminInfo {
72    pub name: String,
73    pub vector_count: u64,
74    #[serde(skip_serializing_if = "Option::is_none")]
75    pub dimension: Option<usize>,
76    pub index_type: String,
77    pub storage_bytes: u64,
78    pub document_count: u64,
79    #[serde(skip_serializing_if = "Option::is_none")]
80    pub created_at: Option<u64>,
81    #[serde(skip_serializing_if = "Option::is_none")]
82    pub updated_at: Option<u64>,
83    pub index_stats: IndexStats,
84}
85
86/// Namespace list response
87#[derive(Debug, Clone, Serialize, Deserialize)]
88pub struct NamespaceListResponse {
89    pub namespaces: Vec<NamespaceAdminInfo>,
90    pub total: u64,
91    pub total_vectors: u64,
92}
93
94/// Optimize namespace request
95#[derive(Debug, Clone, Serialize, Deserialize)]
96pub struct OptimizeRequest {
97    #[serde(default)]
98    pub force: bool,
99    #[serde(skip_serializing_if = "Option::is_none")]
100    pub target_index_type: Option<String>,
101}
102
103/// Optimize namespace response
104#[derive(Debug, Clone, Serialize, Deserialize)]
105pub struct OptimizeResponse {
106    pub success: bool,
107    #[serde(skip_serializing_if = "Option::is_none")]
108    pub job_id: Option<String>,
109    pub message: String,
110}
111
112// ============================================================================
113// Index Admin Types
114// ============================================================================
115
116/// Index statistics for all namespaces
117#[derive(Debug, Clone, Serialize, Deserialize)]
118pub struct IndexStatsResponse {
119    pub namespaces: HashMap<String, IndexStats>,
120    pub total_indexed_vectors: u64,
121    pub total_size_bytes: u64,
122}
123
124/// Rebuild index request
125#[derive(Debug, Clone, Serialize, Deserialize)]
126pub struct RebuildIndexRequest {
127    #[serde(skip_serializing_if = "Option::is_none")]
128    pub namespace: Option<String>,
129    #[serde(skip_serializing_if = "Option::is_none")]
130    pub index_type: Option<String>,
131    #[serde(default)]
132    pub force: bool,
133}
134
135/// Rebuild index response
136#[derive(Debug, Clone, Serialize, Deserialize)]
137pub struct RebuildIndexResponse {
138    pub success: bool,
139    pub job_id: String,
140    pub message: String,
141}
142
143// ============================================================================
144// Cache Admin Types
145// ============================================================================
146
147/// Cache statistics
148#[derive(Debug, Clone, Serialize, Deserialize)]
149pub struct CacheStats {
150    pub enabled: bool,
151    pub cache_type: String,
152    pub entries: u64,
153    pub size_bytes: u64,
154    pub hits: u64,
155    pub misses: u64,
156    pub hit_rate: f64,
157    pub evictions: u64,
158}
159
160/// Clear cache request
161#[derive(Debug, Clone, Serialize, Deserialize)]
162pub struct ClearCacheRequest {
163    #[serde(skip_serializing_if = "Option::is_none")]
164    pub namespace: Option<String>,
165}
166
167/// Clear cache response
168#[derive(Debug, Clone, Serialize, Deserialize)]
169pub struct ClearCacheResponse {
170    pub success: bool,
171    pub entries_cleared: u64,
172    pub message: String,
173}
174
175// ============================================================================
176// Configuration Types
177// ============================================================================
178
179/// Runtime configuration
180#[derive(Debug, Clone, Serialize, Deserialize)]
181pub struct RuntimeConfig {
182    #[serde(skip_serializing_if = "Option::is_none")]
183    pub max_vectors_per_namespace: Option<u64>,
184    pub default_index_type: String,
185    pub cache_enabled: bool,
186    pub cache_max_size_bytes: u64,
187    pub rate_limit_enabled: bool,
188    pub rate_limit_rps: u32,
189    pub query_timeout_ms: u64,
190    /// Whether AutoPilot background tasks (dedup + consolidation) are enabled
191    #[serde(default = "default_true")]
192    pub autopilot_enabled: bool,
193    /// Cosine-similarity threshold for AutoPilot deduplication (0.0–1.0)
194    #[serde(default = "default_dedup_threshold")]
195    pub autopilot_dedup_threshold: f32,
196    /// How often AutoPilot deduplication runs (hours)
197    #[serde(default = "default_dedup_interval")]
198    pub autopilot_dedup_interval_hours: u64,
199    /// How often AutoPilot consolidation runs (hours)
200    #[serde(default = "default_consolidation_interval")]
201    pub autopilot_consolidation_interval_hours: u64,
202}
203
204fn default_true() -> bool {
205    true
206}
207fn default_dedup_threshold() -> f32 {
208    0.93
209}
210fn default_dedup_interval() -> u64 {
211    6
212}
213fn default_consolidation_interval() -> u64 {
214    12
215}
216
217/// Update configuration response
218#[derive(Debug, Clone, Serialize, Deserialize)]
219pub struct UpdateConfigResponse {
220    pub success: bool,
221    pub config: RuntimeConfig,
222    pub message: String,
223    #[serde(default, skip_serializing_if = "Vec::is_empty")]
224    pub warnings: Vec<String>,
225}
226
227// ============================================================================
228// Quota Types
229// ============================================================================
230
231/// Quota configuration
232#[derive(Debug, Clone, Serialize, Deserialize)]
233pub struct QuotaConfig {
234    #[serde(skip_serializing_if = "Option::is_none")]
235    pub max_vectors: Option<u64>,
236    #[serde(skip_serializing_if = "Option::is_none")]
237    pub max_storage_bytes: Option<u64>,
238    #[serde(skip_serializing_if = "Option::is_none")]
239    pub max_queries_per_minute: Option<u64>,
240    #[serde(skip_serializing_if = "Option::is_none")]
241    pub max_writes_per_minute: Option<u64>,
242}
243
244/// Quota usage
245#[derive(Debug, Clone, Serialize, Deserialize)]
246pub struct QuotaUsage {
247    #[serde(default)]
248    pub current_vectors: u64,
249    #[serde(default)]
250    pub current_storage_bytes: u64,
251    #[serde(default)]
252    pub queries_this_minute: u64,
253    #[serde(default)]
254    pub writes_this_minute: u64,
255}
256
257/// Quota status for a namespace
258#[derive(Debug, Clone, Serialize, Deserialize)]
259pub struct QuotaStatus {
260    pub namespace: String,
261    pub config: QuotaConfig,
262    pub usage: QuotaUsage,
263}
264
265/// Quota list response
266#[derive(Debug, Clone, Serialize, Deserialize)]
267pub struct QuotaListResponse {
268    pub quotas: Vec<QuotaStatus>,
269    pub total: u64,
270    #[serde(skip_serializing_if = "Option::is_none")]
271    pub default_config: Option<QuotaConfig>,
272}
273
274// ============================================================================
275// Slow Query Types
276// ============================================================================
277
278/// Slow query entry
279#[derive(Debug, Clone, Serialize, Deserialize)]
280pub struct SlowQueryEntry {
281    pub id: String,
282    pub timestamp: u64,
283    pub namespace: String,
284    pub query_type: String,
285    pub duration_ms: f64,
286    #[serde(default)]
287    pub parameters: Option<serde_json::Value>,
288    #[serde(default)]
289    pub results_count: u64,
290    #[serde(default)]
291    pub vectors_scanned: u64,
292}
293
294/// Slow query list response
295#[derive(Debug, Clone, Serialize, Deserialize)]
296pub struct SlowQueryListResponse {
297    pub queries: Vec<SlowQueryEntry>,
298    pub total: u64,
299    pub threshold_ms: f64,
300}
301
302// ============================================================================
303// Backup Types
304// ============================================================================
305
306/// Backup information
307#[derive(Debug, Clone, Serialize, Deserialize)]
308pub struct BackupInfo {
309    pub backup_id: String,
310    pub name: String,
311    pub backup_type: String,
312    pub status: String,
313    pub namespaces: Vec<String>,
314    pub vector_count: u64,
315    pub size_bytes: u64,
316    pub created_at: u64,
317    #[serde(skip_serializing_if = "Option::is_none")]
318    pub completed_at: Option<u64>,
319    #[serde(skip_serializing_if = "Option::is_none")]
320    pub duration_seconds: Option<u64>,
321    #[serde(skip_serializing_if = "Option::is_none")]
322    pub storage_path: Option<String>,
323    #[serde(skip_serializing_if = "Option::is_none")]
324    pub error: Option<String>,
325    pub encrypted: bool,
326    #[serde(skip_serializing_if = "Option::is_none")]
327    pub compression: Option<String>,
328}
329
330/// List backups response
331#[derive(Debug, Clone, Serialize, Deserialize)]
332pub struct BackupListResponse {
333    pub backups: Vec<BackupInfo>,
334    pub total: u64,
335}
336
337/// Create backup request
338#[derive(Debug, Clone, Serialize, Deserialize)]
339pub struct CreateBackupRequest {
340    pub name: String,
341    #[serde(skip_serializing_if = "Option::is_none")]
342    pub backup_type: Option<String>,
343    #[serde(skip_serializing_if = "Option::is_none")]
344    pub namespaces: Option<Vec<String>>,
345    #[serde(skip_serializing_if = "Option::is_none")]
346    pub encrypt: Option<bool>,
347    #[serde(skip_serializing_if = "Option::is_none")]
348    pub compression: Option<String>,
349}
350
351/// Create backup response
352#[derive(Debug, Clone, Serialize, Deserialize)]
353pub struct CreateBackupResponse {
354    pub backup: BackupInfo,
355    #[serde(skip_serializing_if = "Option::is_none")]
356    pub estimated_completion: Option<u64>,
357}
358
359/// Restore backup request
360#[derive(Debug, Clone, Serialize, Deserialize)]
361pub struct RestoreBackupRequest {
362    pub backup_id: String,
363    #[serde(skip_serializing_if = "Option::is_none")]
364    pub target_namespaces: Option<Vec<String>>,
365    #[serde(skip_serializing_if = "Option::is_none")]
366    pub overwrite: Option<bool>,
367    #[serde(skip_serializing_if = "Option::is_none")]
368    pub point_in_time: Option<u64>,
369}
370
371/// Restore backup response
372#[derive(Debug, Clone, Serialize, Deserialize)]
373pub struct RestoreBackupResponse {
374    pub restore_id: String,
375    pub status: String,
376    pub backup_id: String,
377    pub namespaces: Vec<String>,
378    pub started_at: u64,
379    #[serde(skip_serializing_if = "Option::is_none")]
380    pub estimated_completion: Option<u64>,
381    #[serde(skip_serializing_if = "Option::is_none")]
382    pub progress_percent: Option<u8>,
383    #[serde(skip_serializing_if = "Option::is_none")]
384    pub vectors_restored: Option<u64>,
385    #[serde(skip_serializing_if = "Option::is_none")]
386    pub completed_at: Option<u64>,
387    #[serde(skip_serializing_if = "Option::is_none")]
388    pub duration_seconds: Option<u64>,
389    #[serde(skip_serializing_if = "Option::is_none")]
390    pub error: Option<String>,
391}
392
393// ============================================================================
394// AutoPilot Types (PILOT-1 / PILOT-2 / PILOT-3)
395// ============================================================================
396
397/// AutoPilot configuration
398#[derive(Debug, Clone, Serialize, Deserialize)]
399pub struct AutoPilotConfig {
400    pub enabled: bool,
401    pub dedup_threshold: f32,
402    pub dedup_interval_hours: u64,
403    pub consolidation_interval_hours: u64,
404}
405
406/// Result snapshot from a deduplication cycle
407#[derive(Debug, Clone, Serialize, Deserialize)]
408pub struct DedupResultSnapshot {
409    pub namespaces_processed: usize,
410    pub memories_scanned: usize,
411    pub duplicates_removed: usize,
412}
413
414/// Result snapshot from a consolidation cycle
415#[derive(Debug, Clone, Serialize, Deserialize)]
416pub struct ConsolidationResultSnapshot {
417    pub namespaces_processed: usize,
418    pub memories_scanned: usize,
419    pub clusters_merged: usize,
420    pub memories_consolidated: usize,
421}
422
423/// PILOT-1: AutoPilot status response
424#[derive(Debug, Clone, Serialize, Deserialize)]
425pub struct AutoPilotStatusResponse {
426    pub config: AutoPilotConfig,
427    #[serde(skip_serializing_if = "Option::is_none")]
428    pub last_dedup_at: Option<u64>,
429    #[serde(skip_serializing_if = "Option::is_none")]
430    pub last_consolidation_at: Option<u64>,
431    #[serde(skip_serializing_if = "Option::is_none")]
432    pub last_dedup: Option<DedupResultSnapshot>,
433    #[serde(skip_serializing_if = "Option::is_none")]
434    pub last_consolidation: Option<ConsolidationResultSnapshot>,
435    pub total_dedup_removed: u64,
436    pub total_consolidated: u64,
437}
438
439/// PILOT-2: AutoPilot configuration update request (all fields optional)
440#[derive(Debug, Clone, Serialize, Deserialize, Default)]
441pub struct AutoPilotConfigRequest {
442    #[serde(skip_serializing_if = "Option::is_none")]
443    pub enabled: Option<bool>,
444    #[serde(skip_serializing_if = "Option::is_none")]
445    pub dedup_threshold: Option<f32>,
446    #[serde(skip_serializing_if = "Option::is_none")]
447    pub dedup_interval_hours: Option<u64>,
448    #[serde(skip_serializing_if = "Option::is_none")]
449    pub consolidation_interval_hours: Option<u64>,
450}
451
452/// PILOT-2: AutoPilot configuration update response
453#[derive(Debug, Clone, Serialize, Deserialize)]
454pub struct AutoPilotConfigResponse {
455    pub success: bool,
456    pub config: AutoPilotConfig,
457    pub message: String,
458}
459
460/// PILOT-3: Trigger action
461#[derive(Debug, Clone, Serialize, Deserialize)]
462#[serde(rename_all = "lowercase")]
463pub enum AutoPilotTriggerAction {
464    Dedup,
465    Consolidate,
466    All,
467}
468
469/// PILOT-3: Trigger request
470#[derive(Debug, Clone, Serialize, Deserialize)]
471pub struct AutoPilotTriggerRequest {
472    pub action: AutoPilotTriggerAction,
473}
474
475/// Dedup result returned by a manual trigger
476#[derive(Debug, Clone, Serialize, Deserialize)]
477pub struct AutoPilotDedupResult {
478    pub namespaces_processed: usize,
479    pub memories_scanned: usize,
480    pub duplicates_removed: usize,
481}
482
483/// Consolidation result returned by a manual trigger
484#[derive(Debug, Clone, Serialize, Deserialize)]
485pub struct AutoPilotConsolidationResult {
486    pub namespaces_processed: usize,
487    pub memories_scanned: usize,
488    pub clusters_merged: usize,
489    pub memories_consolidated: usize,
490}
491
492/// PILOT-3: Trigger response
493#[derive(Debug, Clone, Serialize, Deserialize)]
494pub struct AutoPilotTriggerResponse {
495    pub success: bool,
496    pub action: AutoPilotTriggerAction,
497    #[serde(skip_serializing_if = "Option::is_none")]
498    pub dedup: Option<AutoPilotDedupResult>,
499    #[serde(skip_serializing_if = "Option::is_none")]
500    pub consolidation: Option<AutoPilotConsolidationResult>,
501    pub message: String,
502}
503
504// ============================================================================
505// TTL Types
506// ============================================================================
507
508/// TTL cleanup request
509#[derive(Debug, Clone, Serialize, Deserialize)]
510pub struct TtlCleanupRequest {
511    #[serde(skip_serializing_if = "Option::is_none")]
512    pub namespace: Option<String>,
513}
514
515/// TTL cleanup response
516#[derive(Debug, Clone, Serialize, Deserialize)]
517pub struct TtlCleanupResponse {
518    pub success: bool,
519    pub vectors_removed: u64,
520    pub namespaces_cleaned: Vec<String>,
521    pub message: String,
522}
523
524/// TTL statistics for a namespace
525#[derive(Debug, Clone, Serialize, Deserialize)]
526pub struct TtlStats {
527    pub namespace: String,
528    pub vectors_with_ttl: u64,
529    pub expiring_within_hour: u64,
530    pub expiring_within_day: u64,
531    pub expired_pending_cleanup: u64,
532}
533
534/// TTL statistics response
535#[derive(Debug, Clone, Serialize, Deserialize)]
536pub struct TtlStatsResponse {
537    pub namespaces: Vec<TtlStats>,
538    pub total_with_ttl: u64,
539    pub total_expired: u64,
540}
541
542// ============================================================================
543// Admin Client Methods
544// ============================================================================
545
546impl DakeraClient {
547    // ====================================================================
548    // Cluster Management
549    // ====================================================================
550
551    /// Get cluster status overview
552    pub async fn cluster_status(&self) -> Result<ClusterStatus> {
553        let url = format!("{}/admin/cluster/status", self.base_url);
554        let response = self.client.get(&url).send().await?;
555        self.handle_response(response).await
556    }
557
558    /// List cluster nodes
559    pub async fn cluster_nodes(&self) -> Result<NodeListResponse> {
560        let url = format!("{}/admin/cluster/nodes", self.base_url);
561        let response = self.client.get(&url).send().await?;
562        self.handle_response(response).await
563    }
564
565    // ====================================================================
566    // Namespace Administration
567    // ====================================================================
568
569    /// List all namespaces with detailed admin statistics
570    pub async fn list_namespaces_admin(&self) -> Result<NamespaceListResponse> {
571        let url = format!("{}/admin/namespaces", self.base_url);
572        let response = self.client.get(&url).send().await?;
573        self.handle_response(response).await
574    }
575
576    /// Delete an entire namespace and all its data
577    pub async fn delete_namespace_admin(&self, namespace: &str) -> Result<serde_json::Value> {
578        let url = format!("{}/admin/namespaces/{}", self.base_url, namespace);
579        let response = self.client.delete(&url).send().await?;
580        self.handle_response(response).await
581    }
582
583    /// Optimize a namespace
584    pub async fn optimize_namespace(
585        &self,
586        namespace: &str,
587        request: OptimizeRequest,
588    ) -> Result<OptimizeResponse> {
589        let url = format!("{}/admin/namespaces/{}/optimize", self.base_url, namespace);
590        let response = self.client.post(&url).json(&request).send().await?;
591        self.handle_response(response).await
592    }
593
594    // ====================================================================
595    // Index Management
596    // ====================================================================
597
598    /// Get index statistics for all namespaces
599    pub async fn index_stats(&self) -> Result<IndexStatsResponse> {
600        let url = format!("{}/admin/indexes/stats", self.base_url);
601        let response = self.client.get(&url).send().await?;
602        self.handle_response(response).await
603    }
604
605    /// Rebuild indexes
606    pub async fn rebuild_indexes(
607        &self,
608        request: RebuildIndexRequest,
609    ) -> Result<RebuildIndexResponse> {
610        let url = format!("{}/admin/indexes/rebuild", self.base_url);
611        let response = self.client.post(&url).json(&request).send().await?;
612        self.handle_response(response).await
613    }
614
615    // ====================================================================
616    // Cache Management
617    // ====================================================================
618
619    /// Get cache statistics
620    pub async fn cache_stats(&self) -> Result<CacheStats> {
621        let url = format!("{}/admin/cache/stats", self.base_url);
622        let response = self.client.get(&url).send().await?;
623        self.handle_response(response).await
624    }
625
626    /// Clear cache, optionally for a specific namespace
627    pub async fn cache_clear(&self, namespace: Option<&str>) -> Result<ClearCacheResponse> {
628        let url = format!("{}/admin/cache/clear", self.base_url);
629        let request = ClearCacheRequest {
630            namespace: namespace.map(|s| s.to_string()),
631        };
632        let response = self.client.post(&url).json(&request).send().await?;
633        self.handle_response(response).await
634    }
635
636    // ====================================================================
637    // Configuration
638    // ====================================================================
639
640    /// Get runtime configuration
641    pub async fn get_config(&self) -> Result<RuntimeConfig> {
642        let url = format!("{}/admin/config", self.base_url);
643        let response = self.client.get(&url).send().await?;
644        self.handle_response(response).await
645    }
646
647    /// Update runtime configuration
648    pub async fn update_config(
649        &self,
650        updates: HashMap<String, serde_json::Value>,
651    ) -> Result<UpdateConfigResponse> {
652        let url = format!("{}/admin/config", self.base_url);
653        let response = self.client.put(&url).json(&updates).send().await?;
654        self.handle_response(response).await
655    }
656
657    // ====================================================================
658    // Quotas
659    // ====================================================================
660
661    /// List all namespace quotas
662    pub async fn get_quotas(&self) -> Result<QuotaListResponse> {
663        let url = format!("{}/admin/quotas", self.base_url);
664        let response = self.client.get(&url).send().await?;
665        self.handle_response(response).await
666    }
667
668    /// Get quota for a specific namespace
669    pub async fn get_quota(&self, namespace: &str) -> Result<QuotaStatus> {
670        let url = format!("{}/admin/quotas/{}", self.base_url, namespace);
671        let response = self.client.get(&url).send().await?;
672        self.handle_response(response).await
673    }
674
675    /// Set quota for a specific namespace
676    pub async fn set_quota(
677        &self,
678        namespace: &str,
679        config: QuotaConfig,
680    ) -> Result<serde_json::Value> {
681        let url = format!("{}/admin/quotas/{}", self.base_url, namespace);
682        let request = serde_json::json!({ "config": config });
683        let response = self.client.put(&url).json(&request).send().await?;
684        self.handle_response(response).await
685    }
686
687    /// Delete quota for a specific namespace
688    pub async fn delete_quota(&self, namespace: &str) -> Result<serde_json::Value> {
689        let url = format!("{}/admin/quotas/{}", self.base_url, namespace);
690        let response = self.client.delete(&url).send().await?;
691        self.handle_response(response).await
692    }
693
694    /// Update quotas (alias for set_quota on default)
695    pub async fn update_quotas(&self, config: Option<QuotaConfig>) -> Result<serde_json::Value> {
696        let url = format!("{}/admin/quotas/default", self.base_url);
697        let request = serde_json::json!({ "config": config });
698        let response = self.client.put(&url).json(&request).send().await?;
699        self.handle_response(response).await
700    }
701
702    // ====================================================================
703    // Slow Queries
704    // ====================================================================
705
706    /// List recent slow queries
707    pub async fn slow_queries(
708        &self,
709        limit: Option<usize>,
710        namespace: Option<&str>,
711        query_type: Option<&str>,
712    ) -> Result<SlowQueryListResponse> {
713        let mut url = format!("{}/admin/slow-queries", self.base_url);
714        let mut params = Vec::new();
715        if let Some(l) = limit {
716            params.push(format!("limit={}", l));
717        }
718        if let Some(ns) = namespace {
719            params.push(format!("namespace={}", ns));
720        }
721        if let Some(qt) = query_type {
722            params.push(format!("query_type={}", qt));
723        }
724        if !params.is_empty() {
725            url.push('?');
726            url.push_str(&params.join("&"));
727        }
728        let response = self.client.get(&url).send().await?;
729        self.handle_response(response).await
730    }
731
732    /// Get slow query summary and patterns
733    pub async fn slow_query_summary(&self) -> Result<serde_json::Value> {
734        let url = format!("{}/admin/slow-queries/summary", self.base_url);
735        let response = self.client.get(&url).send().await?;
736        self.handle_response(response).await
737    }
738
739    /// Clear slow query log
740    pub async fn clear_slow_queries(&self) -> Result<serde_json::Value> {
741        let url = format!("{}/admin/slow-queries", self.base_url);
742        let response = self.client.delete(&url).send().await?;
743        self.handle_response(response).await
744    }
745
746    // ====================================================================
747    // Backups
748    // ====================================================================
749
750    /// Create a new backup
751    pub async fn create_backup(
752        &self,
753        request: CreateBackupRequest,
754    ) -> Result<CreateBackupResponse> {
755        let url = format!("{}/admin/backups", self.base_url);
756        let response = self.client.post(&url).json(&request).send().await?;
757        self.handle_response(response).await
758    }
759
760    /// List all backups
761    pub async fn list_backups(&self) -> Result<BackupListResponse> {
762        let url = format!("{}/admin/backups", self.base_url);
763        let response = self.client.get(&url).send().await?;
764        self.handle_response(response).await
765    }
766
767    /// Get backup details by ID
768    pub async fn get_backup(&self, backup_id: &str) -> Result<BackupInfo> {
769        let url = format!("{}/admin/backups/{}", self.base_url, backup_id);
770        let response = self.client.get(&url).send().await?;
771        self.handle_response(response).await
772    }
773
774    /// Restore from a backup
775    pub async fn restore_backup(
776        &self,
777        request: RestoreBackupRequest,
778    ) -> Result<RestoreBackupResponse> {
779        let url = format!("{}/admin/backups/restore", self.base_url);
780        let response = self.client.post(&url).json(&request).send().await?;
781        self.handle_response(response).await
782    }
783
784    /// Delete a backup
785    pub async fn delete_backup(&self, backup_id: &str) -> Result<serde_json::Value> {
786        let url = format!("{}/admin/backups/{}", self.base_url, backup_id);
787        let response = self.client.delete(&url).send().await?;
788        self.handle_response(response).await
789    }
790
791    // ====================================================================
792    // TTL Management
793    // ====================================================================
794
795    /// Run TTL cleanup on expired vectors
796    pub async fn ttl_cleanup(&self, namespace: Option<&str>) -> Result<TtlCleanupResponse> {
797        let url = format!("{}/admin/ttl/cleanup", self.base_url);
798        let request = TtlCleanupRequest {
799            namespace: namespace.map(|s| s.to_string()),
800        };
801        let response = self.client.post(&url).json(&request).send().await?;
802        self.handle_response(response).await
803    }
804
805    /// Get TTL statistics
806    pub async fn ttl_stats(&self) -> Result<TtlStatsResponse> {
807        let url = format!("{}/admin/ttl/stats", self.base_url);
808        let response = self.client.get(&url).send().await?;
809        self.handle_response(response).await
810    }
811
812    // ====================================================================
813    // AutoPilot Management (PILOT-1 / PILOT-2 / PILOT-3)
814    // ====================================================================
815
816    /// Get AutoPilot status: current config and last-run statistics (PILOT-1)
817    pub async fn autopilot_status(&self) -> Result<AutoPilotStatusResponse> {
818        let url = format!("{}/admin/autopilot/status", self.base_url);
819        let response = self.client.get(&url).send().await?;
820        self.handle_response(response).await
821    }
822
823    /// Update AutoPilot configuration at runtime (PILOT-2)
824    ///
825    /// All fields are optional — omit any field to keep its current value.
826    pub async fn autopilot_update_config(
827        &self,
828        request: AutoPilotConfigRequest,
829    ) -> Result<AutoPilotConfigResponse> {
830        let url = format!("{}/admin/autopilot/config", self.base_url);
831        let response = self.client.put(&url).json(&request).send().await?;
832        self.handle_response(response).await
833    }
834
835    /// Manually trigger an AutoPilot dedup or consolidation cycle (PILOT-3)
836    ///
837    /// Use `AutoPilotTriggerAction::Dedup`, `::Consolidate`, or `::All`.
838    /// The cycle runs synchronously and returns inline results.
839    pub async fn autopilot_trigger(
840        &self,
841        action: AutoPilotTriggerAction,
842    ) -> Result<AutoPilotTriggerResponse> {
843        let url = format!("{}/admin/autopilot/trigger", self.base_url);
844        let request = AutoPilotTriggerRequest { action };
845        let response = self.client.post(&url).json(&request).send().await?;
846        self.handle_response(response).await
847    }
848}