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