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