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    /// Configure TTL for a namespace.
892    pub async fn configure_ttl(
893        &self,
894        namespace: &str,
895        ttl_seconds: u64,
896        strategy: Option<&str>,
897    ) -> Result<serde_json::Value> {
898        let url = format!("{}/v1/admin/namespaces/{}/ttl", self.base_url, namespace);
899        let mut body = serde_json::json!({ "ttl_seconds": ttl_seconds });
900        if let Some(s) = strategy {
901            body["strategy"] = serde_json::Value::String(s.to_string());
902        }
903        let response = self.client.post(&url).json(&body).send().await?;
904        self.handle_response(response).await
905    }
906
907    /// Run TTL cleanup on expired vectors
908    pub async fn ttl_cleanup(&self, namespace: Option<&str>) -> Result<TtlCleanupResponse> {
909        let url = format!("{}/admin/ttl/cleanup", self.base_url);
910        let request = TtlCleanupRequest {
911            namespace: namespace.map(|s| s.to_string()),
912        };
913        let response = self.client.post(&url).json(&request).send().await?;
914        self.handle_response(response).await
915    }
916
917    /// Get TTL statistics
918    pub async fn ttl_stats(&self) -> Result<TtlStatsResponse> {
919        let url = format!("{}/admin/ttl/stats", self.base_url);
920        let response = self.client.get(&url).send().await?;
921        self.handle_response(response).await
922    }
923
924    // ====================================================================
925    // AutoPilot Management (PILOT-1 / PILOT-2 / PILOT-3)
926    // ====================================================================
927
928    /// Get AutoPilot status: current config and last-run statistics (PILOT-1)
929    pub async fn autopilot_status(&self) -> Result<AutoPilotStatusResponse> {
930        let url = format!("{}/admin/autopilot/status", self.base_url);
931        let response = self.client.get(&url).send().await?;
932        self.handle_response(response).await
933    }
934
935    /// Update AutoPilot configuration at runtime (PILOT-2)
936    ///
937    /// All fields are optional — omit any field to keep its current value.
938    pub async fn autopilot_update_config(
939        &self,
940        request: AutoPilotConfigRequest,
941    ) -> Result<AutoPilotConfigResponse> {
942        let url = format!("{}/admin/autopilot/config", self.base_url);
943        let response = self.client.put(&url).json(&request).send().await?;
944        self.handle_response(response).await
945    }
946
947    /// Manually trigger an AutoPilot dedup or consolidation cycle (PILOT-3)
948    ///
949    /// Use `AutoPilotTriggerAction::Dedup`, `::Consolidate`, or `::All`.
950    /// The cycle runs synchronously and returns inline results.
951    pub async fn autopilot_trigger(
952        &self,
953        action: AutoPilotTriggerAction,
954    ) -> Result<AutoPilotTriggerResponse> {
955        let url = format!("{}/admin/autopilot/trigger", self.base_url);
956        let request = AutoPilotTriggerRequest { action };
957        let response = self.client.post(&url).json(&request).send().await?;
958        self.handle_response(response).await
959    }
960
961    // ====================================================================
962    // Decay Engine Management (DECAY-1 / DECAY-2)
963    // ====================================================================
964
965    /// Get current decay engine configuration (DECAY-1).
966    ///
967    /// Returns the active strategy, half-life, and minimum importance threshold.
968    /// Requires Admin scope.
969    pub async fn decay_config(&self) -> Result<DecayConfigResponse> {
970        let url = format!("{}/admin/decay/config", self.base_url);
971        let response = self.client.get(&url).send().await?;
972        self.handle_response(response).await
973    }
974
975    /// Update decay engine configuration at runtime (DECAY-1).
976    ///
977    /// Changes take effect on the next decay cycle — no restart required.
978    /// All fields are optional; omit any to keep its current value.
979    /// Requires Admin scope.
980    pub async fn decay_update_config(
981        &self,
982        request: DecayConfigUpdateRequest,
983    ) -> Result<DecayConfigUpdateResponse> {
984        let url = format!("{}/admin/decay/config", self.base_url);
985        let response = self.client.put(&url).json(&request).send().await?;
986        self.handle_response(response).await
987    }
988
989    /// Get decay activity counters and last-cycle snapshot (DECAY-2).
990    ///
991    /// Returns cumulative totals (memories decayed/deleted, cycles run) and
992    /// per-cycle statistics from the most recent run. Requires Admin scope.
993    pub async fn decay_stats(&self) -> Result<DecayStatsResponse> {
994        let url = format!("{}/admin/decay/stats", self.base_url);
995        let response = self.client.get(&url).send().await?;
996        self.handle_response(response).await
997    }
998
999    // ====================================================================
1000    // Product KPI Snapshot (OBS-2)
1001    // ====================================================================
1002
1003    /// Return a point-in-time product KPI snapshot (OBS-2).
1004    ///
1005    /// Calls `GET /v1/kpis`. Returns 8 operational metrics covering latency,
1006    /// error rate, and retention. Sub-millisecond — served from in-memory
1007    /// counters. Requires Admin scope.
1008    pub async fn get_kpis(&self) -> Result<KpiSnapshot> {
1009        let url = format!("{}/kpis", self.base_url);
1010        let response = self.client.get(&url).send().await?;
1011        self.handle_response(response).await
1012    }
1013
1014    // ========================================================================
1015    // CE-54: Fulltext Reindex
1016    // ========================================================================
1017
1018    /// Backfill the BM25 fulltext index for memories stored before CE-12 auto-indexing (CE-54).
1019    ///
1020    /// Calls `POST /admin/fulltext/reindex`. Requires Admin scope.
1021    ///
1022    /// Scans all memories in `namespace` (or every agent namespace when `None`) and adds
1023    /// any missing from the BM25 index. Safe to call multiple times — already-indexed
1024    /// memories are counted in `total_skipped` and not re-processed.
1025    pub async fn admin_fulltext_reindex(
1026        &self,
1027        namespace: Option<&str>,
1028    ) -> Result<FulltextReindexResponse> {
1029        let url = format!("{}/admin/fulltext/reindex", self.base_url);
1030        let body = serde_json::json!({ "namespace": namespace });
1031        let response = self.client.post(&url).json(&body).send().await?;
1032        self.handle_response(response).await
1033    }
1034
1035    // =========================================================================
1036    // Cluster & Maintenance
1037    // =========================================================================
1038
1039    /// GET /admin/cluster/replication — cluster replication status.
1040    pub async fn admin_cluster_replication(&self) -> Result<crate::types::ReplicationStatus> {
1041        let url = format!("{}/admin/cluster/replication", self.base_url);
1042        let response = self.client.get(&url).send().await?;
1043        self.handle_response(response).await
1044    }
1045
1046    /// GET /admin/cluster/shards — list shards.
1047    pub async fn admin_list_shards(&self) -> Result<crate::types::ShardListResponse> {
1048        let url = format!("{}/admin/cluster/shards", self.base_url);
1049        let response = self.client.get(&url).send().await?;
1050        self.handle_response(response).await
1051    }
1052
1053    /// POST /admin/cluster/shards/rebalance — rebalance shards.
1054    pub async fn admin_rebalance_shards(
1055        &self,
1056        request: crate::types::ShardRebalanceRequest,
1057    ) -> Result<crate::types::ShardRebalanceResponse> {
1058        let url = format!("{}/admin/cluster/shards/rebalance", self.base_url);
1059        let response = self.client.post(&url).json(&request).send().await?;
1060        self.handle_response(response).await
1061    }
1062
1063    /// GET /admin/cluster/maintenance — maintenance mode status.
1064    pub async fn admin_maintenance_status(&self) -> Result<crate::types::MaintenanceStatus> {
1065        let url = format!("{}/admin/cluster/maintenance", self.base_url);
1066        let response = self.client.get(&url).send().await?;
1067        self.handle_response(response).await
1068    }
1069
1070    /// POST /admin/cluster/maintenance/enable — enable maintenance mode.
1071    pub async fn admin_enable_maintenance(
1072        &self,
1073        request: crate::types::EnableMaintenanceRequest,
1074    ) -> Result<crate::types::MaintenanceStatus> {
1075        let url = format!("{}/admin/cluster/maintenance/enable", self.base_url);
1076        let response = self.client.post(&url).json(&request).send().await?;
1077        self.handle_response(response).await
1078    }
1079
1080    /// POST /admin/cluster/maintenance/disable — disable maintenance mode.
1081    pub async fn admin_disable_maintenance(
1082        &self,
1083        request: crate::types::DisableMaintenanceRequest,
1084    ) -> Result<crate::types::MaintenanceStatus> {
1085        let url = format!("{}/admin/cluster/maintenance/disable", self.base_url);
1086        let response = self.client.post(&url).json(&request).send().await?;
1087        self.handle_response(response).await
1088    }
1089
1090    // =========================================================================
1091    // Quotas
1092    // =========================================================================
1093
1094    /// GET /admin/quotas — list all namespace quotas.
1095    pub async fn admin_list_quotas(&self) -> Result<crate::types::QuotaListResponse> {
1096        let url = format!("{}/admin/quotas", self.base_url);
1097        let response = self.client.get(&url).send().await?;
1098        self.handle_response(response).await
1099    }
1100
1101    /// GET /admin/quotas/default — get default quota configuration.
1102    pub async fn admin_get_default_quota(&self) -> Result<crate::types::DefaultQuotaResponse> {
1103        let url = format!("{}/admin/quotas/default", self.base_url);
1104        let response = self.client.get(&url).send().await?;
1105        self.handle_response(response).await
1106    }
1107
1108    /// PUT /admin/quotas/default — set default quota configuration.
1109    pub async fn admin_set_default_quota(
1110        &self,
1111        request: crate::types::SetDefaultQuotaRequest,
1112    ) -> Result<crate::types::SetQuotaResponse> {
1113        let url = format!("{}/admin/quotas/default", self.base_url);
1114        let response = self.client.put(&url).json(&request).send().await?;
1115        self.handle_response(response).await
1116    }
1117
1118    /// GET /admin/quotas/{namespace} — get namespace quota.
1119    pub async fn admin_get_quota(&self, namespace: &str) -> Result<crate::types::QuotaStatus> {
1120        let url = format!("{}/admin/quotas/{}", self.base_url, namespace);
1121        let response = self.client.get(&url).send().await?;
1122        self.handle_response(response).await
1123    }
1124
1125    /// PUT /admin/quotas/{namespace} — set namespace quota.
1126    pub async fn admin_set_quota(
1127        &self,
1128        namespace: &str,
1129        request: crate::types::SetQuotaRequest,
1130    ) -> Result<crate::types::SetQuotaResponse> {
1131        let url = format!("{}/admin/quotas/{}", self.base_url, namespace);
1132        let response = self.client.put(&url).json(&request).send().await?;
1133        self.handle_response(response).await
1134    }
1135
1136    /// DELETE /admin/quotas/{namespace} — remove namespace quota.
1137    pub async fn admin_delete_quota(&self, namespace: &str) -> Result<serde_json::Value> {
1138        let url = format!("{}/admin/quotas/{}", self.base_url, namespace);
1139        let response = self.client.delete(&url).send().await?;
1140        self.handle_response(response).await
1141    }
1142
1143    /// POST /admin/quotas/{namespace}/check — check if operation would exceed quota.
1144    pub async fn admin_check_quota(
1145        &self,
1146        namespace: &str,
1147        request: crate::types::QuotaCheckRequest,
1148    ) -> Result<crate::types::QuotaCheckResult> {
1149        let url = format!("{}/admin/quotas/{}/check", self.base_url, namespace);
1150        let response = self.client.post(&url).json(&request).send().await?;
1151        self.handle_response(response).await
1152    }
1153
1154    // =========================================================================
1155    // Slow Queries
1156    // =========================================================================
1157
1158    /// GET /admin/slow-queries — list recent slow queries.
1159    pub async fn admin_list_slow_queries(
1160        &self,
1161        namespace: Option<&str>,
1162        query_type: Option<&str>,
1163        limit: Option<u32>,
1164    ) -> Result<Vec<serde_json::Value>> {
1165        let mut url = format!("{}/admin/slow-queries", self.base_url);
1166        let mut params = Vec::new();
1167        if let Some(ns) = namespace {
1168            params.push(format!("namespace={}", ns));
1169        }
1170        if let Some(qt) = query_type {
1171            params.push(format!("query_type={}", qt));
1172        }
1173        if let Some(l) = limit {
1174            params.push(format!("limit={}", l));
1175        }
1176        if !params.is_empty() {
1177            url.push('?');
1178            url.push_str(&params.join("&"));
1179        }
1180        let response = self.client.get(&url).send().await?;
1181        self.handle_response(response).await
1182    }
1183
1184    /// GET /admin/slow-queries/summary — slow query summary.
1185    pub async fn admin_slow_query_summary(&self) -> Result<serde_json::Value> {
1186        let url = format!("{}/admin/slow-queries/summary", self.base_url);
1187        let response = self.client.get(&url).send().await?;
1188        self.handle_response(response).await
1189    }
1190
1191    /// DELETE /admin/slow-queries — clear slow query log.
1192    pub async fn admin_clear_slow_queries(
1193        &self,
1194        namespace: Option<&str>,
1195    ) -> Result<serde_json::Value> {
1196        let mut url = format!("{}/admin/slow-queries", self.base_url);
1197        if let Some(ns) = namespace {
1198            url.push_str(&format!("?namespace={}", ns));
1199        }
1200        let response = self.client.delete(&url).send().await?;
1201        self.handle_response(response).await
1202    }
1203
1204    /// PATCH /admin/slow-queries/config — update slow query configuration.
1205    pub async fn admin_update_slow_query_config(
1206        &self,
1207        config: serde_json::Value,
1208    ) -> Result<serde_json::Value> {
1209        let url = format!("{}/admin/slow-queries/config", self.base_url);
1210        let response = self.client.patch(&url).json(&config).send().await?;
1211        self.handle_response(response).await
1212    }
1213
1214    // =========================================================================
1215    // Backups
1216    // =========================================================================
1217
1218    /// GET /admin/backups — list all backups.
1219    pub async fn admin_list_backups(&self) -> Result<crate::types::BackupListResponse> {
1220        let url = format!("{}/admin/backups", self.base_url);
1221        let response = self.client.get(&url).send().await?;
1222        self.handle_response(response).await
1223    }
1224
1225    /// POST /admin/backups — create a new backup.
1226    pub async fn admin_create_backup(
1227        &self,
1228        request: crate::types::CreateBackupRequest,
1229    ) -> Result<crate::types::CreateBackupResponse> {
1230        let url = format!("{}/admin/backups", self.base_url);
1231        let response = self.client.post(&url).json(&request).send().await?;
1232        self.handle_response(response).await
1233    }
1234
1235    /// GET /admin/backups/{id} — get backup details.
1236    pub async fn admin_get_backup(&self, backup_id: &str) -> Result<crate::types::AdminBackupInfo> {
1237        let url = format!("{}/admin/backups/{}", self.base_url, backup_id);
1238        let response = self.client.get(&url).send().await?;
1239        self.handle_response(response).await
1240    }
1241
1242    /// DELETE /admin/backups/{id} — delete a backup.
1243    pub async fn admin_delete_backup(&self, backup_id: &str) -> Result<serde_json::Value> {
1244        let url = format!("{}/admin/backups/{}", self.base_url, backup_id);
1245        let response = self.client.delete(&url).send().await?;
1246        self.handle_response(response).await
1247    }
1248
1249    /// GET /admin/backups/schedule — get backup schedule.
1250    pub async fn admin_get_backup_schedule(&self) -> Result<crate::types::BackupSchedule> {
1251        let url = format!("{}/admin/backups/schedule", self.base_url);
1252        let response = self.client.get(&url).send().await?;
1253        self.handle_response(response).await
1254    }
1255
1256    /// POST /admin/backups/schedule — update backup schedule.
1257    pub async fn admin_update_backup_schedule(
1258        &self,
1259        request: crate::types::UpdateBackupScheduleRequest,
1260    ) -> Result<crate::types::BackupSchedule> {
1261        let url = format!("{}/admin/backups/schedule", self.base_url);
1262        let response = self.client.post(&url).json(&request).send().await?;
1263        self.handle_response(response).await
1264    }
1265
1266    /// POST /admin/backups/restore — restore from backup.
1267    pub async fn admin_restore_backup(
1268        &self,
1269        request: crate::types::RestoreBackupRequest,
1270    ) -> Result<crate::types::RestoreBackupResponse> {
1271        let url = format!("{}/admin/backups/restore", self.base_url);
1272        let response = self.client.post(&url).json(&request).send().await?;
1273        self.handle_response(response).await
1274    }
1275
1276    /// GET /admin/backups/restore/{id} — restore operation status.
1277    pub async fn admin_get_restore_status(
1278        &self,
1279        restore_id: &str,
1280    ) -> Result<crate::types::RestoreBackupResponse> {
1281        let url = format!("{}/admin/backups/restore/{}", self.base_url, restore_id);
1282        let response = self.client.get(&url).send().await?;
1283        self.handle_response(response).await
1284    }
1285
1286    // =========================================================================
1287    // Ops — Diagnostics & Jobs
1288    // =========================================================================
1289
1290    /// GET /ops/diagnostics — system diagnostics.
1291    pub async fn ops_diagnostics(&self) -> Result<serde_json::Value> {
1292        let url = format!("{}/ops/diagnostics", self.base_url);
1293        let response = self.client.get(&url).send().await?;
1294        self.handle_response(response).await
1295    }
1296
1297    /// GET /ops/jobs — list background jobs.
1298    pub async fn ops_list_jobs(&self) -> Result<Vec<crate::types::JobInfo>> {
1299        let url = format!("{}/ops/jobs", self.base_url);
1300        let response = self.client.get(&url).send().await?;
1301        self.handle_response(response).await
1302    }
1303
1304    /// GET /ops/jobs/{id} — get job status.
1305    pub async fn ops_get_job(&self, job_id: &str) -> Result<crate::types::JobInfo> {
1306        let url = format!("{}/ops/jobs/{}", self.base_url, job_id);
1307        let response = self.client.get(&url).send().await?;
1308        self.handle_response(response).await
1309    }
1310
1311    /// POST /ops/compact — trigger compaction.
1312    pub async fn ops_compact(
1313        &self,
1314        request: crate::types::CompactionRequest,
1315    ) -> Result<crate::types::CompactionResponse> {
1316        let url = format!("{}/ops/compact", self.base_url);
1317        let response = self.client.post(&url).json(&request).send().await?;
1318        self.handle_response(response).await
1319    }
1320
1321    /// POST /ops/shutdown — request graceful shutdown.
1322    pub async fn ops_shutdown(&self) -> Result<serde_json::Value> {
1323        let url = format!("{}/ops/shutdown", self.base_url);
1324        let response = self.client.post(&url).send().await?;
1325        self.handle_response(response).await
1326    }
1327
1328    // =========================================================================
1329    // Backup Download / Upload
1330    // =========================================================================
1331
1332    /// Download a backup as gzipped bytes via `GET /admin/backups/{id}/download`.
1333    pub async fn download_backup(&self, backup_id: &str) -> Result<Vec<u8>> {
1334        let url = format!("{}/admin/backups/{}/download", self.base_url, backup_id);
1335        let response = self.client.get(&url).send().await?;
1336        if !response.status().is_success() {
1337            let status = response.status();
1338            let body = response.text().await.unwrap_or_default();
1339            return Err(crate::error::ClientError::Server {
1340                status: status.as_u16(),
1341                message: body,
1342                code: None,
1343            });
1344        }
1345        Ok(response.bytes().await?.to_vec())
1346    }
1347
1348    /// Upload a backup from gzipped bytes via `POST /admin/backups/upload`.
1349    pub async fn upload_backup(&self, data: Vec<u8>) -> Result<crate::types::CreateBackupResponse> {
1350        let url = format!("{}/admin/backups/upload", self.base_url);
1351        let response = self
1352            .client
1353            .post(&url)
1354            .header("Content-Type", "application/gzip")
1355            .body(data)
1356            .send()
1357            .await?;
1358        self.handle_response(response).await
1359    }
1360
1361    // =========================================================================
1362    // Storage Tier Overview
1363    // =========================================================================
1364
1365    /// Get storage tier overview via `GET /admin/storage/tiers`.
1366    pub async fn storage_tier_overview(&self) -> Result<crate::types::StorageTierOverview> {
1367        let url = format!("{}/admin/storage/tiers", self.base_url);
1368        let response = self.client.get(&url).send().await?;
1369        self.handle_response(response).await
1370    }
1371
1372    // =========================================================================
1373    // Background Activity
1374    // =========================================================================
1375
1376    /// Get background activity metrics via `GET /admin/background-activity`.
1377    pub async fn background_activity(&self) -> Result<serde_json::Value> {
1378        let url = format!("{}/admin/background-activity", self.base_url);
1379        let response = self.client.get(&url).send().await?;
1380        self.handle_response(response).await
1381    }
1382
1383    // =========================================================================
1384    // Memory Type Stats
1385    // =========================================================================
1386
1387    /// Get per-type memory statistics via `GET /admin/memory-type-stats`.
1388    pub async fn memory_type_stats(&self) -> Result<crate::types::MemoryTypeStatsResponse> {
1389        let url = format!("{}/admin/memory-type-stats", self.base_url);
1390        let response = self.client.get(&url).send().await?;
1391        self.handle_response(response).await
1392    }
1393
1394    // =========================================================================
1395    // Migrate Namespace Dimensions
1396    // =========================================================================
1397
1398    /// Migrate namespace embedding dimensions via `POST /admin/namespaces/migrate-dimensions`.
1399    pub async fn migrate_namespace_dimensions(
1400        &self,
1401        request: crate::types::MigrateNamespaceDimensionsRequest,
1402    ) -> Result<crate::types::MigrateDimensionsResponse> {
1403        let url = format!("{}/admin/namespaces/migrate-dimensions", self.base_url);
1404        let response = self.client.post(&url).json(&request).send().await?;
1405        self.handle_response(response).await
1406    }
1407}
1408
1409// ============================================================================
1410// Product KPI Snapshot (OBS-2)
1411// ============================================================================
1412
1413/// Point-in-time product KPI snapshot returned by `GET /v1/kpis` (OBS-2).
1414///
1415/// All latency values are in milliseconds. Rate/percentage values are in the
1416/// range `0.0`–`100.0`. Integer counts are unsigned.
1417///
1418/// Requires Admin scope.
1419#[derive(Debug, Clone, Serialize, Deserialize)]
1420pub struct KpiSnapshot {
1421    /// Median recall latency across all namespaces over the last minute (ms).
1422    pub recall_latency_p50_ms: f64,
1423    /// 99th-percentile recall latency across all namespaces over the last minute (ms).
1424    pub recall_latency_p99_ms: f64,
1425    /// Median store latency across all namespaces over the last minute (ms).
1426    pub store_latency_p50_ms: f64,
1427    /// 5xx error rate as a percentage of total API requests over the last minute.
1428    pub api_error_rate_5xx_pct: f64,
1429    /// Distinct agent identifiers that stored or recalled a memory in the last 24 hours.
1430    pub active_agents_count: u64,
1431    /// Total sessions created in the rolling 7-day window.
1432    pub session_count_week: u64,
1433    /// Current number of nodes in the cross-agent knowledge graph.
1434    pub cross_agent_network_node_count: u64,
1435    /// Percentage of memories created 7 days ago that are still active.
1436    pub memory_retention_7d_pct: f64,
1437}
1438
1439// ============================================================================
1440// CE-54: Fulltext Reindex (Admin)
1441// ============================================================================
1442
1443/// Per-namespace result from `POST /admin/fulltext/reindex` (CE-54).
1444#[derive(Debug, Clone, Serialize, Deserialize)]
1445pub struct FulltextReindexNamespaceResult {
1446    /// Namespace that was scanned.
1447    pub namespace: String,
1448    /// Total vectors examined.
1449    pub vectors_scanned: usize,
1450    /// Memories newly added to the BM25 index.
1451    pub newly_indexed: usize,
1452    /// Memories already in the BM25 index (skipped).
1453    pub already_indexed: usize,
1454    /// Memories that could not be parsed.
1455    pub parse_failures: usize,
1456}
1457
1458/// Response from `POST /admin/fulltext/reindex` (CE-54).
1459///
1460/// Returned by [`DakeraClient::admin_fulltext_reindex`].
1461#[derive(Debug, Clone, Serialize, Deserialize)]
1462pub struct FulltextReindexResponse {
1463    /// Number of namespaces scanned.
1464    pub namespaces_processed: usize,
1465    /// Total memories newly added to BM25 across all namespaces.
1466    pub total_indexed: usize,
1467    /// Total memories already in the BM25 index (skipped).
1468    pub total_skipped: usize,
1469    /// Per-namespace breakdown.
1470    pub details: Vec<FulltextReindexNamespaceResult>,
1471}