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// Decay Engine Types (DECAY-1 / DECAY-2)
506// ============================================================================
507
508/// DECAY-1: Current decay configuration
509#[derive(Debug, Clone, Serialize, Deserialize)]
510pub struct DecayConfigResponse {
511    /// Decay strategy: "exponential", "linear", or "step"
512    pub strategy: String,
513    /// Half-life in hours
514    pub half_life_hours: f64,
515    /// Minimum importance threshold; memories below are hard-deleted on next cycle
516    pub min_importance: f32,
517}
518
519/// DECAY-1: Runtime configuration update request (all fields optional)
520#[derive(Debug, Clone, Serialize, Deserialize, Default)]
521pub struct DecayConfigUpdateRequest {
522    /// Decay strategy: "exponential", "linear", or "step"
523    #[serde(skip_serializing_if = "Option::is_none")]
524    pub strategy: Option<String>,
525    /// Half-life in hours (must be > 0)
526    #[serde(skip_serializing_if = "Option::is_none")]
527    pub half_life_hours: Option<f64>,
528    /// Minimum importance threshold 0.0–1.0
529    #[serde(skip_serializing_if = "Option::is_none")]
530    pub min_importance: Option<f32>,
531}
532
533/// DECAY-1: Runtime configuration update response
534#[derive(Debug, Clone, Serialize, Deserialize)]
535pub struct DecayConfigUpdateResponse {
536    pub success: bool,
537    pub config: DecayConfigResponse,
538    pub message: String,
539}
540
541/// DECAY-2: Stats from a single decay cycle
542#[derive(Debug, Clone, Serialize, Deserialize)]
543pub struct LastDecayCycleStats {
544    pub namespaces_processed: usize,
545    pub memories_processed: usize,
546    pub memories_decayed: usize,
547    pub memories_deleted: usize,
548}
549
550/// DECAY-2: Decay activity counters and last-cycle snapshot
551#[derive(Debug, Clone, Serialize, Deserialize)]
552pub struct DecayStatsResponse {
553    /// Total memories whose importance was lowered by decay (all-time)
554    pub total_decayed: u64,
555    /// Total memories hard-deleted by decay or TTL expiry (all-time)
556    pub total_deleted: u64,
557    /// Unix timestamp of the last decay cycle (None if never run)
558    #[serde(skip_serializing_if = "Option::is_none")]
559    pub last_run_at: Option<u64>,
560    /// Number of decay cycles completed since startup
561    pub cycles_run: u64,
562    /// Stats from the most recent decay cycle (None if never run)
563    #[serde(skip_serializing_if = "Option::is_none")]
564    pub last_cycle: Option<LastDecayCycleStats>,
565}
566
567// ============================================================================
568// TTL Types
569// ============================================================================
570
571/// TTL cleanup request
572#[derive(Debug, Clone, Serialize, Deserialize)]
573pub struct TtlCleanupRequest {
574    #[serde(skip_serializing_if = "Option::is_none")]
575    pub namespace: Option<String>,
576}
577
578/// TTL cleanup response
579#[derive(Debug, Clone, Serialize, Deserialize)]
580pub struct TtlCleanupResponse {
581    pub success: bool,
582    pub vectors_removed: u64,
583    pub namespaces_cleaned: Vec<String>,
584    pub message: String,
585}
586
587/// TTL statistics for a namespace
588#[derive(Debug, Clone, Serialize, Deserialize)]
589pub struct TtlStats {
590    pub namespace: String,
591    pub vectors_with_ttl: u64,
592    pub expiring_within_hour: u64,
593    pub expiring_within_day: u64,
594    pub expired_pending_cleanup: u64,
595}
596
597/// TTL statistics response
598#[derive(Debug, Clone, Serialize, Deserialize)]
599pub struct TtlStatsResponse {
600    pub namespaces: Vec<TtlStats>,
601    pub total_with_ttl: u64,
602    pub total_expired: u64,
603}
604
605// ============================================================================
606// Admin Client Methods
607// ============================================================================
608
609impl DakeraClient {
610    // ====================================================================
611    // Cluster Management
612    // ====================================================================
613
614    /// Get cluster status overview
615    pub async fn cluster_status(&self) -> Result<ClusterStatus> {
616        let url = format!("{}/admin/cluster/status", self.base_url);
617        let response = self.client.get(&url).send().await?;
618        self.handle_response(response).await
619    }
620
621    /// List cluster nodes
622    pub async fn cluster_nodes(&self) -> Result<NodeListResponse> {
623        let url = format!("{}/admin/cluster/nodes", self.base_url);
624        let response = self.client.get(&url).send().await?;
625        self.handle_response(response).await
626    }
627
628    // ====================================================================
629    // Namespace Administration
630    // ====================================================================
631
632    /// List all namespaces with detailed admin statistics
633    pub async fn list_namespaces_admin(&self) -> Result<NamespaceListResponse> {
634        let url = format!("{}/admin/namespaces", self.base_url);
635        let response = self.client.get(&url).send().await?;
636        self.handle_response(response).await
637    }
638
639    /// Delete an entire namespace and all its data
640    pub async fn delete_namespace_admin(&self, namespace: &str) -> Result<serde_json::Value> {
641        let url = format!("{}/admin/namespaces/{}", self.base_url, namespace);
642        let response = self.client.delete(&url).send().await?;
643        self.handle_response(response).await
644    }
645
646    /// Optimize a namespace
647    pub async fn optimize_namespace(
648        &self,
649        namespace: &str,
650        request: OptimizeRequest,
651    ) -> Result<OptimizeResponse> {
652        let url = format!("{}/admin/namespaces/{}/optimize", self.base_url, namespace);
653        let response = self.client.post(&url).json(&request).send().await?;
654        self.handle_response(response).await
655    }
656
657    // ====================================================================
658    // Index Management
659    // ====================================================================
660
661    /// Get index statistics for all namespaces
662    pub async fn index_stats(&self) -> Result<IndexStatsResponse> {
663        let url = format!("{}/admin/indexes/stats", self.base_url);
664        let response = self.client.get(&url).send().await?;
665        self.handle_response(response).await
666    }
667
668    /// Rebuild indexes
669    pub async fn rebuild_indexes(
670        &self,
671        request: RebuildIndexRequest,
672    ) -> Result<RebuildIndexResponse> {
673        let url = format!("{}/admin/indexes/rebuild", self.base_url);
674        let response = self.client.post(&url).json(&request).send().await?;
675        self.handle_response(response).await
676    }
677
678    // ====================================================================
679    // Cache Management
680    // ====================================================================
681
682    /// Get cache statistics
683    pub async fn cache_stats(&self) -> Result<CacheStats> {
684        let url = format!("{}/admin/cache/stats", self.base_url);
685        let response = self.client.get(&url).send().await?;
686        self.handle_response(response).await
687    }
688
689    /// Clear cache, optionally for a specific namespace
690    pub async fn cache_clear(&self, namespace: Option<&str>) -> Result<ClearCacheResponse> {
691        let url = format!("{}/admin/cache/clear", self.base_url);
692        let request = ClearCacheRequest {
693            namespace: namespace.map(|s| s.to_string()),
694        };
695        let response = self.client.post(&url).json(&request).send().await?;
696        self.handle_response(response).await
697    }
698
699    // ====================================================================
700    // Configuration
701    // ====================================================================
702
703    /// Get runtime configuration
704    pub async fn get_config(&self) -> Result<RuntimeConfig> {
705        let url = format!("{}/admin/config", self.base_url);
706        let response = self.client.get(&url).send().await?;
707        self.handle_response(response).await
708    }
709
710    /// Update runtime configuration
711    pub async fn update_config(
712        &self,
713        updates: HashMap<String, serde_json::Value>,
714    ) -> Result<UpdateConfigResponse> {
715        let url = format!("{}/admin/config", self.base_url);
716        let response = self.client.put(&url).json(&updates).send().await?;
717        self.handle_response(response).await
718    }
719
720    // ====================================================================
721    // Quotas
722    // ====================================================================
723
724    /// List all namespace quotas
725    pub async fn get_quotas(&self) -> Result<QuotaListResponse> {
726        let url = format!("{}/admin/quotas", self.base_url);
727        let response = self.client.get(&url).send().await?;
728        self.handle_response(response).await
729    }
730
731    /// Get quota for a specific namespace
732    pub async fn get_quota(&self, namespace: &str) -> Result<QuotaStatus> {
733        let url = format!("{}/admin/quotas/{}", self.base_url, namespace);
734        let response = self.client.get(&url).send().await?;
735        self.handle_response(response).await
736    }
737
738    /// Set quota for a specific namespace
739    pub async fn set_quota(
740        &self,
741        namespace: &str,
742        config: QuotaConfig,
743    ) -> Result<serde_json::Value> {
744        let url = format!("{}/admin/quotas/{}", self.base_url, namespace);
745        let request = serde_json::json!({ "config": config });
746        let response = self.client.put(&url).json(&request).send().await?;
747        self.handle_response(response).await
748    }
749
750    /// Delete quota for a specific namespace
751    pub async fn delete_quota(&self, namespace: &str) -> Result<serde_json::Value> {
752        let url = format!("{}/admin/quotas/{}", self.base_url, namespace);
753        let response = self.client.delete(&url).send().await?;
754        self.handle_response(response).await
755    }
756
757    /// Update quotas (alias for set_quota on default)
758    pub async fn update_quotas(&self, config: Option<QuotaConfig>) -> Result<serde_json::Value> {
759        let url = format!("{}/admin/quotas/default", self.base_url);
760        let request = serde_json::json!({ "config": config });
761        let response = self.client.put(&url).json(&request).send().await?;
762        self.handle_response(response).await
763    }
764
765    // ====================================================================
766    // Slow Queries
767    // ====================================================================
768
769    /// List recent slow queries
770    pub async fn slow_queries(
771        &self,
772        limit: Option<usize>,
773        namespace: Option<&str>,
774        query_type: Option<&str>,
775    ) -> Result<SlowQueryListResponse> {
776        let mut url = format!("{}/admin/slow-queries", self.base_url);
777        let mut params = Vec::new();
778        if let Some(l) = limit {
779            params.push(format!("limit={}", l));
780        }
781        if let Some(ns) = namespace {
782            params.push(format!("namespace={}", ns));
783        }
784        if let Some(qt) = query_type {
785            params.push(format!("query_type={}", qt));
786        }
787        if !params.is_empty() {
788            url.push('?');
789            url.push_str(&params.join("&"));
790        }
791        let response = self.client.get(&url).send().await?;
792        self.handle_response(response).await
793    }
794
795    /// Get slow query summary and patterns
796    pub async fn slow_query_summary(&self) -> Result<serde_json::Value> {
797        let url = format!("{}/admin/slow-queries/summary", self.base_url);
798        let response = self.client.get(&url).send().await?;
799        self.handle_response(response).await
800    }
801
802    /// Clear slow query log
803    pub async fn clear_slow_queries(&self) -> Result<serde_json::Value> {
804        let url = format!("{}/admin/slow-queries", self.base_url);
805        let response = self.client.delete(&url).send().await?;
806        self.handle_response(response).await
807    }
808
809    // ====================================================================
810    // Backups
811    // ====================================================================
812
813    /// Create a new backup
814    pub async fn create_backup(
815        &self,
816        request: CreateBackupRequest,
817    ) -> Result<CreateBackupResponse> {
818        let url = format!("{}/admin/backups", self.base_url);
819        let response = self.client.post(&url).json(&request).send().await?;
820        self.handle_response(response).await
821    }
822
823    /// List all backups
824    pub async fn list_backups(&self) -> Result<BackupListResponse> {
825        let url = format!("{}/admin/backups", self.base_url);
826        let response = self.client.get(&url).send().await?;
827        self.handle_response(response).await
828    }
829
830    /// Get backup details by ID
831    pub async fn get_backup(&self, backup_id: &str) -> Result<BackupInfo> {
832        let url = format!("{}/admin/backups/{}", self.base_url, backup_id);
833        let response = self.client.get(&url).send().await?;
834        self.handle_response(response).await
835    }
836
837    /// Restore from a backup
838    pub async fn restore_backup(
839        &self,
840        request: RestoreBackupRequest,
841    ) -> Result<RestoreBackupResponse> {
842        let url = format!("{}/admin/backups/restore", self.base_url);
843        let response = self.client.post(&url).json(&request).send().await?;
844        self.handle_response(response).await
845    }
846
847    /// Delete a backup
848    pub async fn delete_backup(&self, backup_id: &str) -> Result<serde_json::Value> {
849        let url = format!("{}/admin/backups/{}", self.base_url, backup_id);
850        let response = self.client.delete(&url).send().await?;
851        self.handle_response(response).await
852    }
853
854    // ====================================================================
855    // TTL Management
856    // ====================================================================
857
858    /// Run TTL cleanup on expired vectors
859    pub async fn ttl_cleanup(&self, namespace: Option<&str>) -> Result<TtlCleanupResponse> {
860        let url = format!("{}/admin/ttl/cleanup", self.base_url);
861        let request = TtlCleanupRequest {
862            namespace: namespace.map(|s| s.to_string()),
863        };
864        let response = self.client.post(&url).json(&request).send().await?;
865        self.handle_response(response).await
866    }
867
868    /// Get TTL statistics
869    pub async fn ttl_stats(&self) -> Result<TtlStatsResponse> {
870        let url = format!("{}/admin/ttl/stats", self.base_url);
871        let response = self.client.get(&url).send().await?;
872        self.handle_response(response).await
873    }
874
875    // ====================================================================
876    // AutoPilot Management (PILOT-1 / PILOT-2 / PILOT-3)
877    // ====================================================================
878
879    /// Get AutoPilot status: current config and last-run statistics (PILOT-1)
880    pub async fn autopilot_status(&self) -> Result<AutoPilotStatusResponse> {
881        let url = format!("{}/admin/autopilot/status", self.base_url);
882        let response = self.client.get(&url).send().await?;
883        self.handle_response(response).await
884    }
885
886    /// Update AutoPilot configuration at runtime (PILOT-2)
887    ///
888    /// All fields are optional — omit any field to keep its current value.
889    pub async fn autopilot_update_config(
890        &self,
891        request: AutoPilotConfigRequest,
892    ) -> Result<AutoPilotConfigResponse> {
893        let url = format!("{}/admin/autopilot/config", self.base_url);
894        let response = self.client.put(&url).json(&request).send().await?;
895        self.handle_response(response).await
896    }
897
898    /// Manually trigger an AutoPilot dedup or consolidation cycle (PILOT-3)
899    ///
900    /// Use `AutoPilotTriggerAction::Dedup`, `::Consolidate`, or `::All`.
901    /// The cycle runs synchronously and returns inline results.
902    pub async fn autopilot_trigger(
903        &self,
904        action: AutoPilotTriggerAction,
905    ) -> Result<AutoPilotTriggerResponse> {
906        let url = format!("{}/admin/autopilot/trigger", self.base_url);
907        let request = AutoPilotTriggerRequest { action };
908        let response = self.client.post(&url).json(&request).send().await?;
909        self.handle_response(response).await
910    }
911
912    // ====================================================================
913    // Decay Engine Management (DECAY-1 / DECAY-2)
914    // ====================================================================
915
916    /// Get current decay engine configuration (DECAY-1).
917    ///
918    /// Returns the active strategy, half-life, and minimum importance threshold.
919    /// Requires Admin scope.
920    pub async fn decay_config(&self) -> Result<DecayConfigResponse> {
921        let url = format!("{}/admin/decay/config", self.base_url);
922        let response = self.client.get(&url).send().await?;
923        self.handle_response(response).await
924    }
925
926    /// Update decay engine configuration at runtime (DECAY-1).
927    ///
928    /// Changes take effect on the next decay cycle — no restart required.
929    /// All fields are optional; omit any to keep its current value.
930    /// Requires Admin scope.
931    pub async fn decay_update_config(
932        &self,
933        request: DecayConfigUpdateRequest,
934    ) -> Result<DecayConfigUpdateResponse> {
935        let url = format!("{}/admin/decay/config", self.base_url);
936        let response = self.client.put(&url).json(&request).send().await?;
937        self.handle_response(response).await
938    }
939
940    /// Get decay activity counters and last-cycle snapshot (DECAY-2).
941    ///
942    /// Returns cumulative totals (memories decayed/deleted, cycles run) and
943    /// per-cycle statistics from the most recent run. Requires Admin scope.
944    pub async fn decay_stats(&self) -> Result<DecayStatsResponse> {
945        let url = format!("{}/admin/decay/stats", self.base_url);
946        let response = self.client.get(&url).send().await?;
947        self.handle_response(response).await
948    }
949}