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!("{}/v1/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!("{}/v1/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!("{}/v1/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!("{}/v1/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!(
686            "{}/v1/admin/namespaces/{}/optimize",
687            self.base_url, namespace
688        );
689        let response = self.client.post(&url).json(&request).send().await?;
690        self.handle_response(response).await
691    }
692
693    // ====================================================================
694    // Index Management
695    // ====================================================================
696
697    /// Get index statistics for all namespaces
698    pub async fn index_stats(&self) -> Result<IndexStatsResponse> {
699        let url = format!("{}/v1/admin/indexes/stats", self.base_url);
700        let response = self.client.get(&url).send().await?;
701        self.handle_response(response).await
702    }
703
704    /// Rebuild indexes
705    pub async fn rebuild_indexes(
706        &self,
707        request: RebuildIndexRequest,
708    ) -> Result<RebuildIndexResponse> {
709        let url = format!("{}/v1/admin/indexes/rebuild", self.base_url);
710        let response = self.client.post(&url).json(&request).send().await?;
711        self.handle_response(response).await
712    }
713
714    // ====================================================================
715    // Cache Management
716    // ====================================================================
717
718    /// Get cache statistics
719    pub async fn cache_stats(&self) -> Result<CacheStats> {
720        let url = format!("{}/v1/admin/cache/stats", self.base_url);
721        let response = self.client.get(&url).send().await?;
722        self.handle_response(response).await
723    }
724
725    /// Clear cache, optionally for a specific namespace
726    pub async fn cache_clear(&self, namespace: Option<&str>) -> Result<ClearCacheResponse> {
727        let url = format!("{}/v1/admin/cache/clear", self.base_url);
728        let request = ClearCacheRequest {
729            namespace: namespace.map(|s| s.to_string()),
730        };
731        let response = self.client.post(&url).json(&request).send().await?;
732        self.handle_response(response).await
733    }
734
735    // ====================================================================
736    // Configuration
737    // ====================================================================
738
739    /// Get runtime configuration
740    pub async fn get_config(&self) -> Result<RuntimeConfig> {
741        let url = format!("{}/v1/admin/config", self.base_url);
742        let response = self.client.get(&url).send().await?;
743        self.handle_response(response).await
744    }
745
746    /// Update runtime configuration
747    pub async fn update_config(
748        &self,
749        updates: HashMap<String, serde_json::Value>,
750    ) -> Result<UpdateConfigResponse> {
751        let url = format!("{}/v1/admin/config", self.base_url);
752        let response = self.client.put(&url).json(&updates).send().await?;
753        self.handle_response(response).await
754    }
755
756    // ====================================================================
757    // Quotas
758    // ====================================================================
759
760    /// List all namespace quotas
761    pub async fn get_quotas(&self) -> Result<QuotaListResponse> {
762        let url = format!("{}/v1/admin/quotas", self.base_url);
763        let response = self.client.get(&url).send().await?;
764        self.handle_response(response).await
765    }
766
767    /// Get quota for a specific namespace
768    pub async fn get_quota(&self, namespace: &str) -> Result<QuotaStatus> {
769        let url = format!("{}/v1/admin/quotas/{}", self.base_url, namespace);
770        let response = self.client.get(&url).send().await?;
771        self.handle_response(response).await
772    }
773
774    /// Set quota for a specific namespace
775    pub async fn set_quota(
776        &self,
777        namespace: &str,
778        config: QuotaConfig,
779    ) -> Result<serde_json::Value> {
780        let url = format!("{}/v1/admin/quotas/{}", self.base_url, namespace);
781        let request = serde_json::json!({ "config": config });
782        let response = self.client.put(&url).json(&request).send().await?;
783        self.handle_response(response).await
784    }
785
786    /// Delete quota for a specific namespace
787    pub async fn delete_quota(&self, namespace: &str) -> Result<serde_json::Value> {
788        let url = format!("{}/v1/admin/quotas/{}", self.base_url, namespace);
789        let response = self.client.delete(&url).send().await?;
790        self.handle_response(response).await
791    }
792
793    /// Update quotas (alias for set_quota on default)
794    pub async fn update_quotas(&self, config: Option<QuotaConfig>) -> Result<serde_json::Value> {
795        let url = format!("{}/v1/admin/quotas/default", self.base_url);
796        let request = serde_json::json!({ "config": config });
797        let response = self.client.put(&url).json(&request).send().await?;
798        self.handle_response(response).await
799    }
800
801    // ====================================================================
802    // Slow Queries
803    // ====================================================================
804
805    /// List recent slow queries
806    pub async fn slow_queries(
807        &self,
808        limit: Option<usize>,
809        namespace: Option<&str>,
810        query_type: Option<&str>,
811    ) -> Result<SlowQueryListResponse> {
812        let mut url = format!("{}/v1/admin/slow-queries", self.base_url);
813        let mut params = Vec::new();
814        if let Some(l) = limit {
815            params.push(format!("limit={}", l));
816        }
817        if let Some(ns) = namespace {
818            params.push(format!("namespace={}", ns));
819        }
820        if let Some(qt) = query_type {
821            params.push(format!("query_type={}", qt));
822        }
823        if !params.is_empty() {
824            url.push('?');
825            url.push_str(&params.join("&"));
826        }
827        let response = self.client.get(&url).send().await?;
828        self.handle_response(response).await
829    }
830
831    /// Get slow query summary and patterns
832    pub async fn slow_query_summary(&self) -> Result<serde_json::Value> {
833        let url = format!("{}/v1/admin/slow-queries/summary", self.base_url);
834        let response = self.client.get(&url).send().await?;
835        self.handle_response(response).await
836    }
837
838    /// Clear slow query log
839    pub async fn clear_slow_queries(&self) -> Result<serde_json::Value> {
840        let url = format!("{}/v1/admin/slow-queries", self.base_url);
841        let response = self.client.delete(&url).send().await?;
842        self.handle_response(response).await
843    }
844
845    // ====================================================================
846    // Backups
847    // ====================================================================
848
849    /// Create a new backup
850    pub async fn create_backup(
851        &self,
852        request: CreateBackupRequest,
853    ) -> Result<CreateBackupResponse> {
854        let url = format!("{}/v1/admin/backups", self.base_url);
855        let response = self.client.post(&url).json(&request).send().await?;
856        self.handle_response(response).await
857    }
858
859    /// List all backups
860    pub async fn list_backups(&self) -> Result<BackupListResponse> {
861        let url = format!("{}/v1/admin/backups", self.base_url);
862        let response = self.client.get(&url).send().await?;
863        self.handle_response(response).await
864    }
865
866    /// Get backup details by ID
867    pub async fn get_backup(&self, backup_id: &str) -> Result<BackupInfo> {
868        let url = format!("{}/v1/admin/backups/{}", self.base_url, backup_id);
869        let response = self.client.get(&url).send().await?;
870        self.handle_response(response).await
871    }
872
873    /// Restore from a backup
874    pub async fn restore_backup(
875        &self,
876        request: RestoreBackupRequest,
877    ) -> Result<RestoreBackupResponse> {
878        let url = format!("{}/v1/admin/backups/restore", self.base_url);
879        let response = self.client.post(&url).json(&request).send().await?;
880        self.handle_response(response).await
881    }
882
883    /// Delete a backup
884    pub async fn delete_backup(&self, backup_id: &str) -> Result<serde_json::Value> {
885        let url = format!("{}/v1/admin/backups/{}", self.base_url, backup_id);
886        let response = self.client.delete(&url).send().await?;
887        self.handle_response(response).await
888    }
889
890    // ====================================================================
891    // TTL Management
892    // ====================================================================
893
894    /// Configure TTL for a namespace.
895    pub async fn configure_ttl(
896        &self,
897        namespace: &str,
898        ttl_seconds: u64,
899        strategy: Option<&str>,
900    ) -> Result<serde_json::Value> {
901        let url = format!("{}/v1/admin/namespaces/{}/ttl", self.base_url, namespace);
902        let mut body = serde_json::json!({ "ttl_seconds": ttl_seconds });
903        if let Some(s) = strategy {
904            body["strategy"] = serde_json::Value::String(s.to_string());
905        }
906        let response = self.client.post(&url).json(&body).send().await?;
907        self.handle_response(response).await
908    }
909
910    /// Run TTL cleanup on expired vectors
911    pub async fn ttl_cleanup(&self, namespace: Option<&str>) -> Result<TtlCleanupResponse> {
912        let url = format!("{}/v1/admin/ttl/cleanup", self.base_url);
913        let request = TtlCleanupRequest {
914            namespace: namespace.map(|s| s.to_string()),
915        };
916        let response = self.client.post(&url).json(&request).send().await?;
917        self.handle_response(response).await
918    }
919
920    /// Get TTL statistics
921    pub async fn ttl_stats(&self) -> Result<TtlStatsResponse> {
922        let url = format!("{}/v1/admin/ttl/stats", self.base_url);
923        let response = self.client.get(&url).send().await?;
924        self.handle_response(response).await
925    }
926
927    // ====================================================================
928    // AutoPilot Management (PILOT-1 / PILOT-2 / PILOT-3)
929    // ====================================================================
930
931    /// Get AutoPilot status: current config and last-run statistics (PILOT-1)
932    pub async fn autopilot_status(&self) -> Result<AutoPilotStatusResponse> {
933        let url = format!("{}/v1/admin/autopilot/status", self.base_url);
934        let response = self.client.get(&url).send().await?;
935        self.handle_response(response).await
936    }
937
938    /// Update AutoPilot configuration at runtime (PILOT-2)
939    ///
940    /// All fields are optional — omit any field to keep its current value.
941    pub async fn autopilot_update_config(
942        &self,
943        request: AutoPilotConfigRequest,
944    ) -> Result<AutoPilotConfigResponse> {
945        let url = format!("{}/v1/admin/autopilot/config", self.base_url);
946        let response = self.client.put(&url).json(&request).send().await?;
947        self.handle_response(response).await
948    }
949
950    /// Manually trigger an AutoPilot dedup or consolidation cycle (PILOT-3)
951    ///
952    /// Use `AutoPilotTriggerAction::Dedup`, `::Consolidate`, or `::All`.
953    /// The cycle runs synchronously and returns inline results.
954    pub async fn autopilot_trigger(
955        &self,
956        action: AutoPilotTriggerAction,
957    ) -> Result<AutoPilotTriggerResponse> {
958        let url = format!("{}/v1/admin/autopilot/trigger", self.base_url);
959        let request = AutoPilotTriggerRequest { action };
960        let response = self.client.post(&url).json(&request).send().await?;
961        self.handle_response(response).await
962    }
963
964    // ====================================================================
965    // Decay Engine Management (DECAY-1 / DECAY-2)
966    // ====================================================================
967
968    /// Get current decay engine configuration (DECAY-1).
969    ///
970    /// Returns the active strategy, half-life, and minimum importance threshold.
971    /// Requires Admin scope.
972    pub async fn decay_config(&self) -> Result<DecayConfigResponse> {
973        let url = format!("{}/v1/admin/decay/config", self.base_url);
974        let response = self.client.get(&url).send().await?;
975        self.handle_response(response).await
976    }
977
978    /// Update decay engine configuration at runtime (DECAY-1).
979    ///
980    /// Changes take effect on the next decay cycle — no restart required.
981    /// All fields are optional; omit any to keep its current value.
982    /// Requires Admin scope.
983    pub async fn decay_update_config(
984        &self,
985        request: DecayConfigUpdateRequest,
986    ) -> Result<DecayConfigUpdateResponse> {
987        let url = format!("{}/v1/admin/decay/config", self.base_url);
988        let response = self.client.put(&url).json(&request).send().await?;
989        self.handle_response(response).await
990    }
991
992    /// Get decay activity counters and last-cycle snapshot (DECAY-2).
993    ///
994    /// Returns cumulative totals (memories decayed/deleted, cycles run) and
995    /// per-cycle statistics from the most recent run. Requires Admin scope.
996    pub async fn decay_stats(&self) -> Result<DecayStatsResponse> {
997        let url = format!("{}/v1/admin/decay/stats", self.base_url);
998        let response = self.client.get(&url).send().await?;
999        self.handle_response(response).await
1000    }
1001
1002    // ====================================================================
1003    // Product KPI Snapshot (OBS-2)
1004    // ====================================================================
1005
1006    /// Return a point-in-time product KPI snapshot (OBS-2).
1007    ///
1008    /// Calls `GET /v1/kpis`. Returns 8 operational metrics covering latency,
1009    /// error rate, and retention. Sub-millisecond — served from in-memory
1010    /// counters. Requires Admin scope.
1011    pub async fn get_kpis(&self) -> Result<KpiSnapshot> {
1012        let url = format!("{}/kpis", self.base_url);
1013        let response = self.client.get(&url).send().await?;
1014        self.handle_response(response).await
1015    }
1016
1017    // ========================================================================
1018    // CE-54: Fulltext Reindex
1019    // ========================================================================
1020
1021    /// Backfill the BM25 fulltext index for memories stored before CE-12 auto-indexing (CE-54).
1022    ///
1023    /// Calls `POST /v1/admin/fulltext/reindex`. Requires Admin scope.
1024    ///
1025    /// Scans all memories in `namespace` (or every agent namespace when `None`) and adds
1026    /// any missing from the BM25 index. Safe to call multiple times — already-indexed
1027    /// memories are counted in `total_skipped` and not re-processed.
1028    pub async fn admin_fulltext_reindex(
1029        &self,
1030        namespace: Option<&str>,
1031    ) -> Result<FulltextReindexResponse> {
1032        let url = format!("{}/v1/admin/fulltext/reindex", self.base_url);
1033        let body = serde_json::json!({ "namespace": namespace });
1034        let response = self.client.post(&url).json(&body).send().await?;
1035        self.handle_response(response).await
1036    }
1037
1038    // =========================================================================
1039    // Cluster & Maintenance
1040    // =========================================================================
1041
1042    /// GET /v1/admin/cluster/replication — cluster replication status.
1043    pub async fn admin_cluster_replication(&self) -> Result<crate::types::ReplicationStatus> {
1044        let url = format!("{}/v1/admin/cluster/replication", self.base_url);
1045        let response = self.client.get(&url).send().await?;
1046        self.handle_response(response).await
1047    }
1048
1049    /// GET /v1/admin/cluster/shards — list shards.
1050    pub async fn admin_list_shards(&self) -> Result<crate::types::ShardListResponse> {
1051        let url = format!("{}/v1/admin/cluster/shards", self.base_url);
1052        let response = self.client.get(&url).send().await?;
1053        self.handle_response(response).await
1054    }
1055
1056    /// POST /v1/admin/cluster/shards/rebalance — rebalance shards.
1057    pub async fn admin_rebalance_shards(
1058        &self,
1059        request: crate::types::ShardRebalanceRequest,
1060    ) -> Result<crate::types::ShardRebalanceResponse> {
1061        let url = format!("{}/v1/admin/cluster/shards/rebalance", self.base_url);
1062        let response = self.client.post(&url).json(&request).send().await?;
1063        self.handle_response(response).await
1064    }
1065
1066    /// GET /v1/admin/cluster/maintenance — maintenance mode status.
1067    pub async fn admin_maintenance_status(&self) -> Result<crate::types::MaintenanceStatus> {
1068        let url = format!("{}/v1/admin/cluster/maintenance", self.base_url);
1069        let response = self.client.get(&url).send().await?;
1070        self.handle_response(response).await
1071    }
1072
1073    /// POST /v1/admin/cluster/maintenance/enable — enable maintenance mode.
1074    pub async fn admin_enable_maintenance(
1075        &self,
1076        request: crate::types::EnableMaintenanceRequest,
1077    ) -> Result<crate::types::MaintenanceStatus> {
1078        let url = format!("{}/v1/admin/cluster/maintenance/enable", self.base_url);
1079        let response = self.client.post(&url).json(&request).send().await?;
1080        self.handle_response(response).await
1081    }
1082
1083    /// POST /v1/admin/cluster/maintenance/disable — disable maintenance mode.
1084    pub async fn admin_disable_maintenance(
1085        &self,
1086        request: crate::types::DisableMaintenanceRequest,
1087    ) -> Result<crate::types::MaintenanceStatus> {
1088        let url = format!("{}/v1/admin/cluster/maintenance/disable", self.base_url);
1089        let response = self.client.post(&url).json(&request).send().await?;
1090        self.handle_response(response).await
1091    }
1092
1093    // =========================================================================
1094    // Quotas
1095    // =========================================================================
1096
1097    /// GET /v1/admin/quotas — list all namespace quotas.
1098    pub async fn admin_list_quotas(&self) -> Result<crate::types::QuotaListResponse> {
1099        let url = format!("{}/v1/admin/quotas", self.base_url);
1100        let response = self.client.get(&url).send().await?;
1101        self.handle_response(response).await
1102    }
1103
1104    /// GET /v1/admin/quotas/default — get default quota configuration.
1105    pub async fn admin_get_default_quota(&self) -> Result<crate::types::DefaultQuotaResponse> {
1106        let url = format!("{}/v1/admin/quotas/default", self.base_url);
1107        let response = self.client.get(&url).send().await?;
1108        self.handle_response(response).await
1109    }
1110
1111    /// PUT /v1/admin/quotas/default — set default quota configuration.
1112    pub async fn admin_set_default_quota(
1113        &self,
1114        request: crate::types::SetDefaultQuotaRequest,
1115    ) -> Result<crate::types::SetQuotaResponse> {
1116        let url = format!("{}/v1/admin/quotas/default", self.base_url);
1117        let response = self.client.put(&url).json(&request).send().await?;
1118        self.handle_response(response).await
1119    }
1120
1121    /// GET /v1/admin/quotas/{namespace} — get namespace quota.
1122    pub async fn admin_get_quota(&self, namespace: &str) -> Result<crate::types::QuotaStatus> {
1123        let url = format!("{}/v1/admin/quotas/{}", self.base_url, namespace);
1124        let response = self.client.get(&url).send().await?;
1125        self.handle_response(response).await
1126    }
1127
1128    /// PUT /v1/admin/quotas/{namespace} — set namespace quota.
1129    pub async fn admin_set_quota(
1130        &self,
1131        namespace: &str,
1132        request: crate::types::SetQuotaRequest,
1133    ) -> Result<crate::types::SetQuotaResponse> {
1134        let url = format!("{}/v1/admin/quotas/{}", self.base_url, namespace);
1135        let response = self.client.put(&url).json(&request).send().await?;
1136        self.handle_response(response).await
1137    }
1138
1139    /// DELETE /v1/admin/quotas/{namespace} — remove namespace quota.
1140    pub async fn admin_delete_quota(&self, namespace: &str) -> Result<serde_json::Value> {
1141        let url = format!("{}/v1/admin/quotas/{}", self.base_url, namespace);
1142        let response = self.client.delete(&url).send().await?;
1143        self.handle_response(response).await
1144    }
1145
1146    /// POST /v1/admin/quotas/{namespace}/check — check if operation would exceed quota.
1147    pub async fn admin_check_quota(
1148        &self,
1149        namespace: &str,
1150        request: crate::types::QuotaCheckRequest,
1151    ) -> Result<crate::types::QuotaCheckResult> {
1152        let url = format!("{}/v1/admin/quotas/{}/check", self.base_url, namespace);
1153        let response = self.client.post(&url).json(&request).send().await?;
1154        self.handle_response(response).await
1155    }
1156
1157    // =========================================================================
1158    // Slow Queries
1159    // =========================================================================
1160
1161    /// GET /v1/admin/slow-queries — list recent slow queries.
1162    pub async fn admin_list_slow_queries(
1163        &self,
1164        namespace: Option<&str>,
1165        query_type: Option<&str>,
1166        limit: Option<u32>,
1167    ) -> Result<Vec<serde_json::Value>> {
1168        let mut url = format!("{}/v1/admin/slow-queries", self.base_url);
1169        let mut params = Vec::new();
1170        if let Some(ns) = namespace {
1171            params.push(format!("namespace={}", ns));
1172        }
1173        if let Some(qt) = query_type {
1174            params.push(format!("query_type={}", qt));
1175        }
1176        if let Some(l) = limit {
1177            params.push(format!("limit={}", l));
1178        }
1179        if !params.is_empty() {
1180            url.push('?');
1181            url.push_str(&params.join("&"));
1182        }
1183        let response = self.client.get(&url).send().await?;
1184        self.handle_response(response).await
1185    }
1186
1187    /// GET /v1/admin/slow-queries/summary — slow query summary.
1188    pub async fn admin_slow_query_summary(&self) -> Result<serde_json::Value> {
1189        let url = format!("{}/v1/admin/slow-queries/summary", self.base_url);
1190        let response = self.client.get(&url).send().await?;
1191        self.handle_response(response).await
1192    }
1193
1194    /// DELETE /v1/admin/slow-queries — clear slow query log.
1195    pub async fn admin_clear_slow_queries(
1196        &self,
1197        namespace: Option<&str>,
1198    ) -> Result<serde_json::Value> {
1199        let mut url = format!("{}/v1/admin/slow-queries", self.base_url);
1200        if let Some(ns) = namespace {
1201            url.push_str(&format!("?namespace={}", ns));
1202        }
1203        let response = self.client.delete(&url).send().await?;
1204        self.handle_response(response).await
1205    }
1206
1207    /// PATCH /v1/admin/slow-queries/config — update slow query configuration.
1208    pub async fn admin_update_slow_query_config(
1209        &self,
1210        config: serde_json::Value,
1211    ) -> Result<serde_json::Value> {
1212        let url = format!("{}/v1/admin/slow-queries/config", self.base_url);
1213        let response = self.client.patch(&url).json(&config).send().await?;
1214        self.handle_response(response).await
1215    }
1216
1217    // =========================================================================
1218    // Backups
1219    // =========================================================================
1220
1221    /// GET /v1/admin/backups — list all backups.
1222    pub async fn admin_list_backups(&self) -> Result<crate::types::BackupListResponse> {
1223        let url = format!("{}/v1/admin/backups", self.base_url);
1224        let response = self.client.get(&url).send().await?;
1225        self.handle_response(response).await
1226    }
1227
1228    /// POST /v1/admin/backups — create a new backup.
1229    pub async fn admin_create_backup(
1230        &self,
1231        request: crate::types::CreateBackupRequest,
1232    ) -> Result<crate::types::CreateBackupResponse> {
1233        let url = format!("{}/v1/admin/backups", self.base_url);
1234        let response = self.client.post(&url).json(&request).send().await?;
1235        self.handle_response(response).await
1236    }
1237
1238    /// GET /v1/admin/backups/{id} — get backup details.
1239    pub async fn admin_get_backup(&self, backup_id: &str) -> Result<crate::types::AdminBackupInfo> {
1240        let url = format!("{}/v1/admin/backups/{}", self.base_url, backup_id);
1241        let response = self.client.get(&url).send().await?;
1242        self.handle_response(response).await
1243    }
1244
1245    /// DELETE /v1/admin/backups/{id} — delete a backup.
1246    pub async fn admin_delete_backup(&self, backup_id: &str) -> Result<serde_json::Value> {
1247        let url = format!("{}/v1/admin/backups/{}", self.base_url, backup_id);
1248        let response = self.client.delete(&url).send().await?;
1249        self.handle_response(response).await
1250    }
1251
1252    /// GET /v1/admin/backups/schedule — get backup schedule.
1253    pub async fn admin_get_backup_schedule(&self) -> Result<crate::types::BackupSchedule> {
1254        let url = format!("{}/v1/admin/backups/schedule", self.base_url);
1255        let response = self.client.get(&url).send().await?;
1256        self.handle_response(response).await
1257    }
1258
1259    /// POST /v1/admin/backups/schedule — update backup schedule.
1260    pub async fn admin_update_backup_schedule(
1261        &self,
1262        request: crate::types::UpdateBackupScheduleRequest,
1263    ) -> Result<crate::types::BackupSchedule> {
1264        let url = format!("{}/v1/admin/backups/schedule", self.base_url);
1265        let response = self.client.post(&url).json(&request).send().await?;
1266        self.handle_response(response).await
1267    }
1268
1269    /// POST /v1/admin/backups/restore — restore from backup.
1270    pub async fn admin_restore_backup(
1271        &self,
1272        request: crate::types::RestoreBackupRequest,
1273    ) -> Result<crate::types::RestoreBackupResponse> {
1274        let url = format!("{}/v1/admin/backups/restore", self.base_url);
1275        let response = self.client.post(&url).json(&request).send().await?;
1276        self.handle_response(response).await
1277    }
1278
1279    /// GET /v1/admin/backups/restore/{id} — restore operation status.
1280    pub async fn admin_get_restore_status(
1281        &self,
1282        restore_id: &str,
1283    ) -> Result<crate::types::RestoreBackupResponse> {
1284        let url = format!("{}/v1/admin/backups/restore/{}", self.base_url, restore_id);
1285        let response = self.client.get(&url).send().await?;
1286        self.handle_response(response).await
1287    }
1288
1289    // =========================================================================
1290    // Ops — Diagnostics & Jobs
1291    // =========================================================================
1292
1293    /// GET /ops/diagnostics — system diagnostics.
1294    pub async fn ops_diagnostics(&self) -> Result<serde_json::Value> {
1295        let url = format!("{}/ops/diagnostics", self.base_url);
1296        let response = self.client.get(&url).send().await?;
1297        self.handle_response(response).await
1298    }
1299
1300    /// GET /ops/jobs — list background jobs.
1301    pub async fn ops_list_jobs(&self) -> Result<Vec<crate::types::JobInfo>> {
1302        let url = format!("{}/ops/jobs", self.base_url);
1303        let response = self.client.get(&url).send().await?;
1304        self.handle_response(response).await
1305    }
1306
1307    /// GET /ops/jobs/{id} — get job status.
1308    pub async fn ops_get_job(&self, job_id: &str) -> Result<crate::types::JobInfo> {
1309        let url = format!("{}/ops/jobs/{}", self.base_url, job_id);
1310        let response = self.client.get(&url).send().await?;
1311        self.handle_response(response).await
1312    }
1313
1314    /// POST /ops/compact — trigger compaction.
1315    pub async fn ops_compact(
1316        &self,
1317        request: crate::types::CompactionRequest,
1318    ) -> Result<crate::types::CompactionResponse> {
1319        let url = format!("{}/ops/compact", self.base_url);
1320        let response = self.client.post(&url).json(&request).send().await?;
1321        self.handle_response(response).await
1322    }
1323
1324    /// POST /ops/shutdown — request graceful shutdown.
1325    pub async fn ops_shutdown(&self) -> Result<serde_json::Value> {
1326        let url = format!("{}/ops/shutdown", self.base_url);
1327        let response = self.client.post(&url).send().await?;
1328        self.handle_response(response).await
1329    }
1330
1331    // =========================================================================
1332    // Backup Download / Upload
1333    // =========================================================================
1334
1335    /// Download a backup as gzipped bytes via `GET /v1/admin/backups/{id}/download`.
1336    pub async fn download_backup(&self, backup_id: &str) -> Result<Vec<u8>> {
1337        let url = format!("{}/v1/admin/backups/{}/download", self.base_url, backup_id);
1338        let response = self.client.get(&url).send().await?;
1339        if !response.status().is_success() {
1340            let status = response.status();
1341            let body = response.text().await.unwrap_or_default();
1342            return Err(crate::error::ClientError::Server {
1343                status: status.as_u16(),
1344                message: body,
1345                code: None,
1346            });
1347        }
1348        Ok(response.bytes().await?.to_vec())
1349    }
1350
1351    /// Upload a backup from gzipped bytes via `POST /v1/admin/backups/upload`.
1352    pub async fn upload_backup(&self, data: Vec<u8>) -> Result<crate::types::CreateBackupResponse> {
1353        let url = format!("{}/v1/admin/backups/upload", self.base_url);
1354        let response = self
1355            .client
1356            .post(&url)
1357            .header("Content-Type", "application/gzip")
1358            .body(data)
1359            .send()
1360            .await?;
1361        self.handle_response(response).await
1362    }
1363
1364    // =========================================================================
1365    // Storage Tier Overview
1366    // =========================================================================
1367
1368    /// Get storage tier overview via `GET /v1/admin/storage/tiers`.
1369    pub async fn storage_tier_overview(&self) -> Result<crate::types::StorageTierOverview> {
1370        let url = format!("{}/v1/admin/storage/tiers", self.base_url);
1371        let response = self.client.get(&url).send().await?;
1372        self.handle_response(response).await
1373    }
1374
1375    // =========================================================================
1376    // Background Activity
1377    // =========================================================================
1378
1379    /// Get background activity metrics via `GET /v1/admin/background-activity`.
1380    pub async fn background_activity(&self) -> Result<serde_json::Value> {
1381        let url = format!("{}/v1/admin/background-activity", self.base_url);
1382        let response = self.client.get(&url).send().await?;
1383        self.handle_response(response).await
1384    }
1385
1386    // =========================================================================
1387    // Memory Type Stats
1388    // =========================================================================
1389
1390    /// Get per-type memory statistics via `GET /v1/admin/memory-type-stats`.
1391    pub async fn memory_type_stats(&self) -> Result<crate::types::MemoryTypeStatsResponse> {
1392        let url = format!("{}/v1/admin/memory-type-stats", self.base_url);
1393        let response = self.client.get(&url).send().await?;
1394        self.handle_response(response).await
1395    }
1396
1397    // =========================================================================
1398    // Migrate Namespace Dimensions
1399    // =========================================================================
1400
1401    /// Migrate namespace embedding dimensions via `POST /v1/admin/namespaces/migrate-dimensions`.
1402    pub async fn migrate_namespace_dimensions(
1403        &self,
1404        request: crate::types::MigrateNamespaceDimensionsRequest,
1405    ) -> Result<crate::types::MigrateDimensionsResponse> {
1406        let url = format!("{}/v1/admin/namespaces/migrate-dimensions", self.base_url);
1407        let response = self.client.post(&url).json(&request).send().await?;
1408        self.handle_response(response).await
1409    }
1410
1411    // =========================================================================
1412    // ReembedJob Force-Drain (v0.11.82+)
1413    // =========================================================================
1414
1415    /// Synchronously drain all static vectors to full ONNX quality via
1416    /// `POST /v1/admin/reembed/drain` (v0.11.82+).
1417    ///
1418    /// Runs the re-embedding upgrade loop until zero `_embedding_kind=static`
1419    /// candidates remain across all namespaces, or `request.timeout_secs` elapses.
1420    /// Requires Admin scope. Useful as a pre-benchmark steady-state gate when
1421    /// `DAKERA_TIERED=1`.
1422    ///
1423    /// A [`DrainReembedResponse::remaining`] of `0` guarantees all vectors are at
1424    /// full ONNX quality.
1425    pub async fn drain_reembed(
1426        &self,
1427        request: crate::types::DrainReembedRequest,
1428    ) -> Result<crate::types::DrainReembedResponse> {
1429        let url = format!("{}/v1/admin/reembed/drain", self.base_url);
1430        let response = self.client.post(&url).json(&request).send().await?;
1431        self.handle_response(response).await
1432    }
1433}
1434
1435// ============================================================================
1436// Product KPI Snapshot (OBS-2)
1437// ============================================================================
1438
1439/// Point-in-time product KPI snapshot returned by `GET /v1/kpis` (OBS-2).
1440///
1441/// All latency values are in milliseconds. Rate/percentage values are in the
1442/// range `0.0`–`100.0`. Integer counts are unsigned.
1443///
1444/// Requires Admin scope.
1445#[derive(Debug, Clone, Serialize, Deserialize)]
1446pub struct KpiSnapshot {
1447    /// Median recall latency across all namespaces over the last minute (ms).
1448    pub recall_latency_p50_ms: f64,
1449    /// 99th-percentile recall latency across all namespaces over the last minute (ms).
1450    pub recall_latency_p99_ms: f64,
1451    /// Median store latency across all namespaces over the last minute (ms).
1452    pub store_latency_p50_ms: f64,
1453    /// 5xx error rate as a percentage of total API requests over the last minute.
1454    pub api_error_rate_5xx_pct: f64,
1455    /// Distinct agent identifiers that stored or recalled a memory in the last 24 hours.
1456    pub active_agents_count: u64,
1457    /// Total sessions created in the rolling 7-day window.
1458    pub session_count_week: u64,
1459    /// Current number of nodes in the cross-agent knowledge graph.
1460    pub cross_agent_network_node_count: u64,
1461    /// Percentage of memories created 7 days ago that are still active.
1462    pub memory_retention_7d_pct: f64,
1463}
1464
1465// ============================================================================
1466// CE-54: Fulltext Reindex (Admin)
1467// ============================================================================
1468
1469/// Per-namespace result from `POST /v1/admin/fulltext/reindex` (CE-54).
1470#[derive(Debug, Clone, Serialize, Deserialize)]
1471pub struct FulltextReindexNamespaceResult {
1472    /// Namespace that was scanned.
1473    pub namespace: String,
1474    /// Total vectors examined.
1475    pub vectors_scanned: usize,
1476    /// Memories newly added to the BM25 index.
1477    pub newly_indexed: usize,
1478    /// Memories already in the BM25 index (skipped).
1479    pub already_indexed: usize,
1480    /// Memories that could not be parsed.
1481    pub parse_failures: usize,
1482}
1483
1484/// Response from `POST /v1/admin/fulltext/reindex` (CE-54).
1485///
1486/// Returned by [`DakeraClient::admin_fulltext_reindex`].
1487#[derive(Debug, Clone, Serialize, Deserialize)]
1488pub struct FulltextReindexResponse {
1489    /// Number of namespaces scanned.
1490    pub namespaces_processed: usize,
1491    /// Total memories newly added to BM25 across all namespaces.
1492    pub total_indexed: usize,
1493    /// Total memories already in the BM25 index (skipped).
1494    pub total_skipped: usize,
1495    /// Per-namespace breakdown.
1496    pub details: Vec<FulltextReindexNamespaceResult>,
1497}