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