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::types::{WarmCacheRequest, WarmCacheResponse};
12use crate::DakeraClient;
13
14// ============================================================================
15// Cluster Types
16// ============================================================================
17
18/// Ops stats response — Read-scoped; works with read-only API keys
19#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct OpsStats {
21    pub version: String,
22    pub total_vectors: u64,
23    pub namespace_count: u64,
24    pub uptime_seconds: u64,
25    pub timestamp: u64,
26    pub state: String,
27}
28
29/// Cluster status response
30#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct ClusterStatus {
32    pub cluster_id: String,
33    pub state: String,
34    pub node_count: u32,
35    pub total_vectors: u64,
36    pub namespace_count: u64,
37    pub version: String,
38    pub timestamp: u64,
39    /// Redis connectivity status (OPS-3).
40    #[serde(default, skip_serializing_if = "Option::is_none")]
41    pub redis_healthy: Option<bool>,
42}
43
44/// Node information
45#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct NodeInfo {
47    pub node_id: String,
48    pub address: String,
49    pub role: String,
50    pub status: String,
51    pub version: String,
52    pub uptime_seconds: u64,
53    pub vector_count: u64,
54    pub memory_bytes: u64,
55    #[serde(default)]
56    pub cpu_percent: f32,
57    #[serde(default)]
58    pub memory_percent: f32,
59    pub last_heartbeat: u64,
60}
61
62/// Node list response
63#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct NodeListResponse {
65    pub nodes: Vec<NodeInfo>,
66    pub total: u32,
67}
68
69// ============================================================================
70// Namespace Admin Types
71// ============================================================================
72
73/// Index statistics
74#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct IndexStats {
76    pub index_type: String,
77    pub is_built: bool,
78    pub size_bytes: u64,
79    pub indexed_vectors: u64,
80    #[serde(skip_serializing_if = "Option::is_none")]
81    pub last_rebuild: Option<u64>,
82}
83
84/// Detailed namespace statistics
85#[derive(Debug, Clone, Serialize, Deserialize)]
86pub struct NamespaceAdminInfo {
87    pub name: String,
88    pub vector_count: u64,
89    #[serde(skip_serializing_if = "Option::is_none")]
90    pub dimension: Option<usize>,
91    pub index_type: String,
92    pub storage_bytes: u64,
93    pub document_count: u64,
94    #[serde(skip_serializing_if = "Option::is_none")]
95    pub created_at: Option<u64>,
96    #[serde(skip_serializing_if = "Option::is_none")]
97    pub updated_at: Option<u64>,
98    pub index_stats: IndexStats,
99}
100
101/// Namespace list response
102#[derive(Debug, Clone, Serialize, Deserialize)]
103pub struct NamespaceListResponse {
104    pub namespaces: Vec<NamespaceAdminInfo>,
105    pub total: u64,
106    pub total_vectors: u64,
107}
108
109/// Optimize namespace request
110#[derive(Debug, Clone, Serialize, Deserialize)]
111pub struct OptimizeRequest {
112    #[serde(default)]
113    pub force: bool,
114    #[serde(skip_serializing_if = "Option::is_none")]
115    pub target_index_type: Option<String>,
116}
117
118/// Optimize namespace response
119#[derive(Debug, Clone, Serialize, Deserialize)]
120pub struct OptimizeResponse {
121    pub success: bool,
122    #[serde(skip_serializing_if = "Option::is_none")]
123    pub job_id: Option<String>,
124    pub message: String,
125}
126
127// ============================================================================
128// Index Admin Types
129// ============================================================================
130
131/// Index statistics for all namespaces
132#[derive(Debug, Clone, Serialize, Deserialize)]
133pub struct IndexStatsResponse {
134    pub namespaces: HashMap<String, IndexStats>,
135    pub total_indexed_vectors: u64,
136    pub total_size_bytes: u64,
137}
138
139/// Rebuild index request
140#[derive(Debug, Clone, Serialize, Deserialize)]
141pub struct RebuildIndexRequest {
142    #[serde(skip_serializing_if = "Option::is_none")]
143    pub namespace: Option<String>,
144    #[serde(skip_serializing_if = "Option::is_none")]
145    pub index_type: Option<String>,
146    #[serde(default)]
147    pub force: bool,
148}
149
150/// Rebuild index response
151#[derive(Debug, Clone, Serialize, Deserialize)]
152pub struct RebuildIndexResponse {
153    pub success: bool,
154    pub job_id: String,
155    pub message: String,
156}
157
158// ============================================================================
159// Cache Admin Types
160// ============================================================================
161
162/// Cache statistics
163#[derive(Debug, Clone, Serialize, Deserialize)]
164pub struct CacheStats {
165    pub enabled: bool,
166    pub cache_type: String,
167    pub entries: u64,
168    pub size_bytes: u64,
169    pub hits: u64,
170    pub misses: u64,
171    pub hit_rate: f64,
172    pub evictions: u64,
173}
174
175/// Clear cache request
176#[derive(Debug, Clone, Serialize, Deserialize)]
177pub struct ClearCacheRequest {
178    #[serde(skip_serializing_if = "Option::is_none")]
179    pub namespace: Option<String>,
180}
181
182/// Clear cache response
183#[derive(Debug, Clone, Serialize, Deserialize)]
184pub struct ClearCacheResponse {
185    pub success: bool,
186    pub entries_cleared: u64,
187    pub message: String,
188}
189
190// ============================================================================
191// Configuration Types
192// ============================================================================
193
194/// Runtime configuration
195#[derive(Debug, Clone, Serialize, Deserialize)]
196pub struct RuntimeConfig {
197    #[serde(skip_serializing_if = "Option::is_none")]
198    pub max_vectors_per_namespace: Option<u64>,
199    pub default_index_type: String,
200    pub cache_enabled: bool,
201    pub cache_max_size_bytes: u64,
202    pub rate_limit_enabled: bool,
203    pub rate_limit_rps: u32,
204    pub query_timeout_ms: u64,
205    /// Whether AutoPilot background tasks (dedup + consolidation) are enabled
206    #[serde(default = "default_true")]
207    pub autopilot_enabled: bool,
208    /// Cosine-similarity threshold for AutoPilot deduplication (0.0–1.0)
209    #[serde(default = "default_dedup_threshold")]
210    pub autopilot_dedup_threshold: f32,
211    /// How often AutoPilot deduplication runs (hours)
212    #[serde(default = "default_dedup_interval")]
213    pub autopilot_dedup_interval_hours: u64,
214    /// How often AutoPilot consolidation runs (hours)
215    #[serde(default = "default_consolidation_interval")]
216    pub autopilot_consolidation_interval_hours: u64,
217}
218
219fn default_true() -> bool {
220    true
221}
222fn default_dedup_threshold() -> f32 {
223    0.93
224}
225fn default_dedup_interval() -> u64 {
226    6
227}
228fn default_consolidation_interval() -> u64 {
229    12
230}
231
232/// Update configuration response
233#[derive(Debug, Clone, Serialize, Deserialize)]
234pub struct UpdateConfigResponse {
235    pub success: bool,
236    pub config: RuntimeConfig,
237    pub message: String,
238    #[serde(default, skip_serializing_if = "Vec::is_empty")]
239    pub warnings: Vec<String>,
240}
241
242// ============================================================================
243// Quota Types
244// ============================================================================
245
246/// Quota configuration
247#[derive(Debug, Clone, Serialize, Deserialize)]
248pub struct QuotaConfig {
249    #[serde(skip_serializing_if = "Option::is_none")]
250    pub max_vectors: Option<u64>,
251    #[serde(skip_serializing_if = "Option::is_none")]
252    pub max_storage_bytes: Option<u64>,
253    #[serde(skip_serializing_if = "Option::is_none")]
254    pub max_queries_per_minute: Option<u64>,
255    #[serde(skip_serializing_if = "Option::is_none")]
256    pub max_writes_per_minute: Option<u64>,
257}
258
259/// Quota usage
260#[derive(Debug, Clone, Serialize, Deserialize)]
261pub struct QuotaUsage {
262    #[serde(default)]
263    pub current_vectors: u64,
264    #[serde(default)]
265    pub current_storage_bytes: u64,
266    #[serde(default)]
267    pub queries_this_minute: u64,
268    #[serde(default)]
269    pub writes_this_minute: u64,
270}
271
272/// Quota status for a namespace
273#[derive(Debug, Clone, Serialize, Deserialize)]
274pub struct QuotaStatus {
275    pub namespace: String,
276    pub config: QuotaConfig,
277    pub usage: QuotaUsage,
278}
279
280/// Quota list response
281#[derive(Debug, Clone, Serialize, Deserialize)]
282pub struct QuotaListResponse {
283    pub quotas: Vec<QuotaStatus>,
284    pub total: u64,
285    #[serde(skip_serializing_if = "Option::is_none")]
286    pub default_config: Option<QuotaConfig>,
287}
288
289// ============================================================================
290// Slow Query Types
291// ============================================================================
292
293/// Slow query entry
294#[derive(Debug, Clone, Serialize, Deserialize)]
295pub struct SlowQueryEntry {
296    pub id: String,
297    pub timestamp: u64,
298    pub namespace: String,
299    pub query_type: String,
300    pub duration_ms: f64,
301    #[serde(default)]
302    pub parameters: Option<serde_json::Value>,
303    #[serde(default)]
304    pub results_count: u64,
305    #[serde(default)]
306    pub vectors_scanned: u64,
307}
308
309/// Slow query list response
310#[derive(Debug, Clone, Serialize, Deserialize)]
311pub struct SlowQueryListResponse {
312    pub queries: Vec<SlowQueryEntry>,
313    pub total: u64,
314    pub threshold_ms: f64,
315}
316
317// ============================================================================
318// Backup Types
319// ============================================================================
320
321/// Backup information
322#[derive(Debug, Clone, Serialize, Deserialize)]
323pub struct BackupInfo {
324    pub backup_id: String,
325    pub name: String,
326    pub backup_type: String,
327    pub status: String,
328    pub namespaces: Vec<String>,
329    pub vector_count: u64,
330    pub size_bytes: u64,
331    pub created_at: u64,
332    #[serde(skip_serializing_if = "Option::is_none")]
333    pub completed_at: Option<u64>,
334    #[serde(skip_serializing_if = "Option::is_none")]
335    pub duration_seconds: Option<u64>,
336    #[serde(skip_serializing_if = "Option::is_none")]
337    pub storage_path: Option<String>,
338    #[serde(skip_serializing_if = "Option::is_none")]
339    pub error: Option<String>,
340    pub encrypted: bool,
341    #[serde(skip_serializing_if = "Option::is_none")]
342    pub compression: Option<String>,
343}
344
345/// List backups response
346#[derive(Debug, Clone, Serialize, Deserialize)]
347pub struct BackupListResponse {
348    pub backups: Vec<BackupInfo>,
349    pub total: u64,
350}
351
352/// Create backup request
353#[derive(Debug, Clone, Serialize, Deserialize)]
354pub struct CreateBackupRequest {
355    pub name: String,
356    #[serde(skip_serializing_if = "Option::is_none")]
357    pub backup_type: Option<String>,
358    #[serde(skip_serializing_if = "Option::is_none")]
359    pub namespaces: Option<Vec<String>>,
360    #[serde(skip_serializing_if = "Option::is_none")]
361    pub encrypt: Option<bool>,
362    #[serde(skip_serializing_if = "Option::is_none")]
363    pub compression: Option<String>,
364}
365
366/// Create backup response
367#[derive(Debug, Clone, Serialize, Deserialize)]
368pub struct CreateBackupResponse {
369    pub backup: BackupInfo,
370    #[serde(skip_serializing_if = "Option::is_none")]
371    pub estimated_completion: Option<u64>,
372}
373
374/// Restore backup request
375#[derive(Debug, Clone, Serialize, Deserialize)]
376pub struct RestoreBackupRequest {
377    pub backup_id: String,
378    #[serde(skip_serializing_if = "Option::is_none")]
379    pub target_namespaces: Option<Vec<String>>,
380    #[serde(skip_serializing_if = "Option::is_none")]
381    pub overwrite: Option<bool>,
382    #[serde(skip_serializing_if = "Option::is_none")]
383    pub point_in_time: Option<u64>,
384}
385
386/// Restore backup response
387#[derive(Debug, Clone, Serialize, Deserialize)]
388pub struct RestoreBackupResponse {
389    pub restore_id: String,
390    pub status: String,
391    pub backup_id: String,
392    pub namespaces: Vec<String>,
393    pub started_at: u64,
394    #[serde(skip_serializing_if = "Option::is_none")]
395    pub estimated_completion: Option<u64>,
396    #[serde(skip_serializing_if = "Option::is_none")]
397    pub progress_percent: Option<u8>,
398    #[serde(skip_serializing_if = "Option::is_none")]
399    pub vectors_restored: Option<u64>,
400    #[serde(skip_serializing_if = "Option::is_none")]
401    pub completed_at: Option<u64>,
402    #[serde(skip_serializing_if = "Option::is_none")]
403    pub duration_seconds: Option<u64>,
404    #[serde(skip_serializing_if = "Option::is_none")]
405    pub error: Option<String>,
406}
407
408// ============================================================================
409// AutoPilot Types (PILOT-1 / PILOT-2 / PILOT-3)
410// ============================================================================
411
412/// AutoPilot configuration
413#[derive(Debug, Clone, Serialize, Deserialize)]
414pub struct AutoPilotConfig {
415    pub enabled: bool,
416    pub dedup_threshold: f32,
417    pub dedup_interval_hours: u64,
418    pub consolidation_interval_hours: u64,
419}
420
421/// Result snapshot from a deduplication cycle
422#[derive(Debug, Clone, Serialize, Deserialize)]
423pub struct DedupResultSnapshot {
424    pub namespaces_processed: usize,
425    pub memories_scanned: usize,
426    pub duplicates_removed: usize,
427}
428
429/// Result snapshot from a consolidation cycle
430#[derive(Debug, Clone, Serialize, Deserialize)]
431pub struct ConsolidationResultSnapshot {
432    pub namespaces_processed: usize,
433    pub memories_scanned: usize,
434    pub clusters_merged: usize,
435    pub memories_consolidated: usize,
436}
437
438/// PILOT-1: AutoPilot status response
439#[derive(Debug, Clone, Serialize, Deserialize)]
440pub struct AutoPilotStatusResponse {
441    pub config: AutoPilotConfig,
442    #[serde(skip_serializing_if = "Option::is_none")]
443    pub last_dedup_at: Option<u64>,
444    #[serde(skip_serializing_if = "Option::is_none")]
445    pub last_consolidation_at: Option<u64>,
446    #[serde(skip_serializing_if = "Option::is_none")]
447    pub last_dedup: Option<DedupResultSnapshot>,
448    #[serde(skip_serializing_if = "Option::is_none")]
449    pub last_consolidation: Option<ConsolidationResultSnapshot>,
450    pub total_dedup_removed: u64,
451    pub total_consolidated: u64,
452}
453
454/// PILOT-2: AutoPilot configuration update request (all fields optional)
455#[derive(Debug, Clone, Serialize, Deserialize, Default)]
456pub struct AutoPilotConfigRequest {
457    #[serde(skip_serializing_if = "Option::is_none")]
458    pub enabled: Option<bool>,
459    #[serde(skip_serializing_if = "Option::is_none")]
460    pub dedup_threshold: Option<f32>,
461    #[serde(skip_serializing_if = "Option::is_none")]
462    pub dedup_interval_hours: Option<u64>,
463    #[serde(skip_serializing_if = "Option::is_none")]
464    pub consolidation_interval_hours: Option<u64>,
465}
466
467/// PILOT-2: AutoPilot configuration update response
468#[derive(Debug, Clone, Serialize, Deserialize)]
469pub struct AutoPilotConfigResponse {
470    pub success: bool,
471    pub config: AutoPilotConfig,
472    pub message: String,
473}
474
475/// PILOT-3: Trigger action
476#[derive(Debug, Clone, Serialize, Deserialize)]
477#[serde(rename_all = "lowercase")]
478pub enum AutoPilotTriggerAction {
479    Dedup,
480    Consolidate,
481    All,
482}
483
484/// PILOT-3: Trigger request
485#[derive(Debug, Clone, Serialize, Deserialize)]
486pub struct AutoPilotTriggerRequest {
487    pub action: AutoPilotTriggerAction,
488}
489
490/// Dedup result returned by a manual trigger
491#[derive(Debug, Clone, Serialize, Deserialize)]
492pub struct AutoPilotDedupResult {
493    pub namespaces_processed: usize,
494    pub memories_scanned: usize,
495    pub duplicates_removed: usize,
496}
497
498/// Consolidation result returned by a manual trigger
499#[derive(Debug, Clone, Serialize, Deserialize)]
500pub struct AutoPilotConsolidationResult {
501    pub namespaces_processed: usize,
502    pub memories_scanned: usize,
503    pub clusters_merged: usize,
504    pub memories_consolidated: usize,
505}
506
507/// PILOT-3: Trigger response
508#[derive(Debug, Clone, Serialize, Deserialize)]
509pub struct AutoPilotTriggerResponse {
510    pub success: bool,
511    pub action: AutoPilotTriggerAction,
512    #[serde(skip_serializing_if = "Option::is_none")]
513    pub dedup: Option<AutoPilotDedupResult>,
514    #[serde(skip_serializing_if = "Option::is_none")]
515    pub consolidation: Option<AutoPilotConsolidationResult>,
516    pub message: String,
517}
518
519// ============================================================================
520// Decay Engine Types (DECAY-1 / DECAY-2)
521// ============================================================================
522
523/// DECAY-1: Current decay configuration
524#[derive(Debug, Clone, Serialize, Deserialize)]
525pub struct DecayConfigResponse {
526    /// Decay strategy: "exponential", "linear", or "step"
527    pub strategy: String,
528    /// Half-life in hours
529    pub half_life_hours: f64,
530    /// Minimum importance threshold; memories below are hard-deleted on next cycle
531    pub min_importance: f32,
532}
533
534/// DECAY-1: Runtime configuration update request (all fields optional)
535#[derive(Debug, Clone, Serialize, Deserialize, Default)]
536pub struct DecayConfigUpdateRequest {
537    /// Decay strategy: "exponential", "linear", or "step"
538    #[serde(skip_serializing_if = "Option::is_none")]
539    pub strategy: Option<String>,
540    /// Half-life in hours (must be > 0)
541    #[serde(skip_serializing_if = "Option::is_none")]
542    pub half_life_hours: Option<f64>,
543    /// Minimum importance threshold 0.0–1.0
544    #[serde(skip_serializing_if = "Option::is_none")]
545    pub min_importance: Option<f32>,
546}
547
548/// DECAY-1: Runtime configuration update response
549#[derive(Debug, Clone, Serialize, Deserialize)]
550pub struct DecayConfigUpdateResponse {
551    pub success: bool,
552    pub config: DecayConfigResponse,
553    pub message: String,
554}
555
556/// DECAY-2: Stats from a single decay cycle
557#[derive(Debug, Clone, Serialize, Deserialize)]
558pub struct LastDecayCycleStats {
559    pub namespaces_processed: usize,
560    pub memories_processed: usize,
561    pub memories_decayed: usize,
562    pub memories_deleted: usize,
563}
564
565/// DECAY-2: Decay activity counters and last-cycle snapshot
566#[derive(Debug, Clone, Serialize, Deserialize)]
567pub struct DecayStatsResponse {
568    /// Total memories whose importance was lowered by decay (all-time)
569    pub total_decayed: u64,
570    /// Total memories hard-deleted by decay or TTL expiry (all-time)
571    pub total_deleted: u64,
572    /// Unix timestamp of the last decay cycle (None if never run)
573    #[serde(skip_serializing_if = "Option::is_none")]
574    pub last_run_at: Option<u64>,
575    /// Number of decay cycles completed since startup
576    pub cycles_run: u64,
577    /// Stats from the most recent decay cycle (None if never run)
578    #[serde(skip_serializing_if = "Option::is_none")]
579    pub last_cycle: Option<LastDecayCycleStats>,
580}
581
582// ============================================================================
583// TTL Types
584// ============================================================================
585
586/// TTL cleanup request
587#[derive(Debug, Clone, Serialize, Deserialize)]
588pub struct TtlCleanupRequest {
589    #[serde(skip_serializing_if = "Option::is_none")]
590    pub namespace: Option<String>,
591}
592
593/// TTL cleanup response
594#[derive(Debug, Clone, Serialize, Deserialize)]
595pub struct TtlCleanupResponse {
596    pub success: bool,
597    pub vectors_removed: u64,
598    pub namespaces_cleaned: Vec<String>,
599    pub message: String,
600}
601
602/// TTL statistics for a namespace
603#[derive(Debug, Clone, Serialize, Deserialize)]
604pub struct TtlStats {
605    pub namespace: String,
606    pub vectors_with_ttl: u64,
607    pub expiring_within_hour: u64,
608    pub expiring_within_day: u64,
609    pub expired_pending_cleanup: u64,
610}
611
612/// TTL statistics response
613#[derive(Debug, Clone, Serialize, Deserialize)]
614pub struct TtlStatsResponse {
615    pub namespaces: Vec<TtlStats>,
616    pub total_with_ttl: u64,
617    pub total_expired: u64,
618}
619
620// ============================================================================
621// Admin Client Methods
622// ============================================================================
623
624impl DakeraClient {
625    // ====================================================================
626    // Cluster Management
627    // ====================================================================
628
629    /// Get server stats (version, total_vectors, namespace_count, uptime_seconds, timestamp).
630    ///
631    /// Requires Read scope — works with read-only API keys, unlike `cluster_status`.
632    pub async fn ops_stats(&self) -> Result<OpsStats> {
633        let url = format!("{}/v1/ops/stats", self.base_url);
634        let response = self.client.get(&url).send().await?;
635        self.handle_response(response).await
636    }
637
638    /// Get Prometheus metrics in text exposition format (INFRA-3).
639    ///
640    /// Requires Admin scope. Returns the raw Prometheus text exposition
641    /// format string suitable for scraping by a Prometheus server.
642    pub async fn ops_metrics(&self) -> Result<String> {
643        let url = format!("{}/v1/ops/metrics", self.base_url);
644        let response = self.client.get(&url).send().await?;
645        self.handle_text_response(response).await
646    }
647
648    /// Get cluster status overview
649    pub async fn cluster_status(&self) -> Result<ClusterStatus> {
650        let url = format!("{}/v1/admin/cluster/status", self.base_url);
651        let response = self.client.get(&url).send().await?;
652        self.handle_response(response).await
653    }
654
655    /// List cluster nodes
656    pub async fn cluster_nodes(&self) -> Result<NodeListResponse> {
657        let url = format!("{}/v1/admin/cluster/nodes", self.base_url);
658        let response = self.client.get(&url).send().await?;
659        self.handle_response(response).await
660    }
661
662    // ====================================================================
663    // Namespace Administration
664    // ====================================================================
665
666    /// List all namespaces with detailed admin statistics
667    pub async fn list_namespaces_admin(&self) -> Result<NamespaceListResponse> {
668        let url = format!("{}/v1/admin/namespaces", self.base_url);
669        let response = self.client.get(&url).send().await?;
670        self.handle_response(response).await
671    }
672
673    /// Delete an entire namespace and all its data
674    pub async fn delete_namespace_admin(&self, namespace: &str) -> Result<serde_json::Value> {
675        let url = format!("{}/v1/admin/namespaces/{}", self.base_url, namespace);
676        let response = self.client.delete(&url).send().await?;
677        self.handle_response(response).await
678    }
679
680    /// Optimize a namespace
681    pub async fn optimize_namespace(
682        &self,
683        namespace: &str,
684        request: OptimizeRequest,
685    ) -> Result<OptimizeResponse> {
686        let url = format!(
687            "{}/v1/admin/namespaces/{}/optimize",
688            self.base_url, namespace
689        );
690        let response = self.client.post(&url).json(&request).send().await?;
691        self.handle_response(response).await
692    }
693
694    // ====================================================================
695    // Index Management
696    // ====================================================================
697
698    /// Get index statistics for all namespaces
699    pub async fn index_stats(&self) -> Result<IndexStatsResponse> {
700        let url = format!("{}/v1/admin/indexes/stats", self.base_url);
701        let response = self.client.get(&url).send().await?;
702        self.handle_response(response).await
703    }
704
705    /// Rebuild indexes
706    pub async fn rebuild_indexes(
707        &self,
708        request: RebuildIndexRequest,
709    ) -> Result<RebuildIndexResponse> {
710        let url = format!("{}/v1/admin/indexes/rebuild", self.base_url);
711        let response = self.client.post(&url).json(&request).send().await?;
712        self.handle_response(response).await
713    }
714
715    // ====================================================================
716    // Cache Management
717    // ====================================================================
718
719    /// Get cache statistics
720    pub async fn cache_stats(&self) -> Result<CacheStats> {
721        let url = format!("{}/v1/admin/cache/stats", self.base_url);
722        let response = self.client.get(&url).send().await?;
723        self.handle_response(response).await
724    }
725
726    /// Clear cache, optionally for a specific namespace
727    pub async fn cache_clear(&self, namespace: Option<&str>) -> Result<ClearCacheResponse> {
728        let url = format!("{}/v1/admin/cache/clear", self.base_url);
729        let request = ClearCacheRequest {
730            namespace: namespace.map(|s| s.to_string()),
731        };
732        let response = self.client.post(&url).json(&request).send().await?;
733        self.handle_response(response).await
734    }
735
736    /// Warm cache for a namespace via `POST /v1/admin/cache/warm`.
737    pub async fn cache_warm(&self, request: WarmCacheRequest) -> Result<WarmCacheResponse> {
738        let url = format!("{}/v1/admin/cache/warm", self.base_url);
739        let response = self.client.post(&url).json(&request).send().await?;
740        self.handle_response(response).await
741    }
742
743    // ====================================================================
744    // Configuration
745    // ====================================================================
746
747    /// Get runtime configuration
748    pub async fn get_config(&self) -> Result<RuntimeConfig> {
749        let url = format!("{}/v1/admin/config", self.base_url);
750        let response = self.client.get(&url).send().await?;
751        self.handle_response(response).await
752    }
753
754    /// Update runtime configuration
755    pub async fn update_config(
756        &self,
757        updates: HashMap<String, serde_json::Value>,
758    ) -> Result<UpdateConfigResponse> {
759        let url = format!("{}/v1/admin/config", self.base_url);
760        let response = self.client.put(&url).json(&updates).send().await?;
761        self.handle_response(response).await
762    }
763
764    // ====================================================================
765    // Quotas
766    // ====================================================================
767
768    /// List all namespace quotas
769    pub async fn get_quotas(&self) -> Result<QuotaListResponse> {
770        let url = format!("{}/v1/admin/quotas", self.base_url);
771        let response = self.client.get(&url).send().await?;
772        self.handle_response(response).await
773    }
774
775    /// Get quota for a specific namespace
776    pub async fn get_quota(&self, namespace: &str) -> Result<QuotaStatus> {
777        let url = format!("{}/v1/admin/quotas/{}", self.base_url, namespace);
778        let response = self.client.get(&url).send().await?;
779        self.handle_response(response).await
780    }
781
782    /// Set quota for a specific namespace
783    pub async fn set_quota(
784        &self,
785        namespace: &str,
786        config: QuotaConfig,
787    ) -> Result<serde_json::Value> {
788        let url = format!("{}/v1/admin/quotas/{}", self.base_url, namespace);
789        let request = serde_json::json!({ "config": config });
790        let response = self.client.put(&url).json(&request).send().await?;
791        self.handle_response(response).await
792    }
793
794    /// Delete quota for a specific namespace
795    pub async fn delete_quota(&self, namespace: &str) -> Result<serde_json::Value> {
796        let url = format!("{}/v1/admin/quotas/{}", self.base_url, namespace);
797        let response = self.client.delete(&url).send().await?;
798        self.handle_response(response).await
799    }
800
801    /// Update quotas (alias for set_quota on default)
802    pub async fn update_quotas(&self, config: Option<QuotaConfig>) -> Result<serde_json::Value> {
803        let url = format!("{}/v1/admin/quotas/default", self.base_url);
804        let request = serde_json::json!({ "config": config });
805        let response = self.client.put(&url).json(&request).send().await?;
806        self.handle_response(response).await
807    }
808
809    // ====================================================================
810    // Slow Queries
811    // ====================================================================
812
813    /// List recent slow queries
814    pub async fn slow_queries(
815        &self,
816        limit: Option<usize>,
817        namespace: Option<&str>,
818        query_type: Option<&str>,
819    ) -> Result<SlowQueryListResponse> {
820        let mut url = format!("{}/v1/admin/slow-queries", self.base_url);
821        let mut params = Vec::new();
822        if let Some(l) = limit {
823            params.push(format!("limit={}", l));
824        }
825        if let Some(ns) = namespace {
826            params.push(format!("namespace={}", ns));
827        }
828        if let Some(qt) = query_type {
829            params.push(format!("query_type={}", qt));
830        }
831        if !params.is_empty() {
832            url.push('?');
833            url.push_str(&params.join("&"));
834        }
835        let response = self.client.get(&url).send().await?;
836        self.handle_response(response).await
837    }
838
839    /// Get slow query summary and patterns
840    pub async fn slow_query_summary(&self) -> Result<serde_json::Value> {
841        let url = format!("{}/v1/admin/slow-queries/summary", self.base_url);
842        let response = self.client.get(&url).send().await?;
843        self.handle_response(response).await
844    }
845
846    /// Clear slow query log
847    pub async fn clear_slow_queries(&self) -> Result<serde_json::Value> {
848        let url = format!("{}/v1/admin/slow-queries", self.base_url);
849        let response = self.client.delete(&url).send().await?;
850        self.handle_response(response).await
851    }
852
853    // ====================================================================
854    // Backups
855    // ====================================================================
856
857    /// Create a new backup
858    pub async fn create_backup(
859        &self,
860        request: CreateBackupRequest,
861    ) -> Result<CreateBackupResponse> {
862        let url = format!("{}/v1/admin/backups", self.base_url);
863        let response = self.client.post(&url).json(&request).send().await?;
864        self.handle_response(response).await
865    }
866
867    /// List all backups
868    pub async fn list_backups(&self) -> Result<BackupListResponse> {
869        let url = format!("{}/v1/admin/backups", self.base_url);
870        let response = self.client.get(&url).send().await?;
871        self.handle_response(response).await
872    }
873
874    /// Get backup details by ID
875    pub async fn get_backup(&self, backup_id: &str) -> Result<BackupInfo> {
876        let url = format!("{}/v1/admin/backups/{}", self.base_url, backup_id);
877        let response = self.client.get(&url).send().await?;
878        self.handle_response(response).await
879    }
880
881    /// Restore from a backup
882    pub async fn restore_backup(
883        &self,
884        request: RestoreBackupRequest,
885    ) -> Result<RestoreBackupResponse> {
886        let url = format!("{}/v1/admin/backups/restore", self.base_url);
887        let response = self.client.post(&url).json(&request).send().await?;
888        self.handle_response(response).await
889    }
890
891    /// Delete a backup
892    pub async fn delete_backup(&self, backup_id: &str) -> Result<serde_json::Value> {
893        let url = format!("{}/v1/admin/backups/{}", self.base_url, backup_id);
894        let response = self.client.delete(&url).send().await?;
895        self.handle_response(response).await
896    }
897
898    // ====================================================================
899    // TTL Management
900    // ====================================================================
901
902    /// Configure TTL for a namespace.
903    pub async fn configure_ttl(
904        &self,
905        namespace: &str,
906        ttl_seconds: u64,
907        strategy: Option<&str>,
908    ) -> Result<serde_json::Value> {
909        let url = format!("{}/v1/admin/namespaces/{}/ttl", self.base_url, namespace);
910        let mut body = serde_json::json!({ "ttl_seconds": ttl_seconds });
911        if let Some(s) = strategy {
912            body["strategy"] = serde_json::Value::String(s.to_string());
913        }
914        let response = self.client.post(&url).json(&body).send().await?;
915        self.handle_response(response).await
916    }
917
918    /// Run TTL cleanup on expired vectors
919    pub async fn ttl_cleanup(&self, namespace: Option<&str>) -> Result<TtlCleanupResponse> {
920        let url = format!("{}/v1/admin/ttl/cleanup", self.base_url);
921        let request = TtlCleanupRequest {
922            namespace: namespace.map(|s| s.to_string()),
923        };
924        let response = self.client.post(&url).json(&request).send().await?;
925        self.handle_response(response).await
926    }
927
928    /// Get TTL statistics
929    pub async fn ttl_stats(&self) -> Result<TtlStatsResponse> {
930        let url = format!("{}/v1/admin/ttl/stats", self.base_url);
931        let response = self.client.get(&url).send().await?;
932        self.handle_response(response).await
933    }
934
935    // ====================================================================
936    // AutoPilot Management (PILOT-1 / PILOT-2 / PILOT-3)
937    // ====================================================================
938
939    /// Get AutoPilot status: current config and last-run statistics (PILOT-1)
940    pub async fn autopilot_status(&self) -> Result<AutoPilotStatusResponse> {
941        let url = format!("{}/v1/admin/autopilot/status", self.base_url);
942        let response = self.client.get(&url).send().await?;
943        self.handle_response(response).await
944    }
945
946    /// Update AutoPilot configuration at runtime (PILOT-2)
947    ///
948    /// All fields are optional — omit any field to keep its current value.
949    pub async fn autopilot_update_config(
950        &self,
951        request: AutoPilotConfigRequest,
952    ) -> Result<AutoPilotConfigResponse> {
953        let url = format!("{}/v1/admin/autopilot/config", self.base_url);
954        let response = self.client.put(&url).json(&request).send().await?;
955        self.handle_response(response).await
956    }
957
958    /// Manually trigger an AutoPilot dedup or consolidation cycle (PILOT-3)
959    ///
960    /// Use `AutoPilotTriggerAction::Dedup`, `::Consolidate`, or `::All`.
961    /// The cycle runs synchronously and returns inline results.
962    pub async fn autopilot_trigger(
963        &self,
964        action: AutoPilotTriggerAction,
965    ) -> Result<AutoPilotTriggerResponse> {
966        let url = format!("{}/v1/admin/autopilot/trigger", self.base_url);
967        let request = AutoPilotTriggerRequest { action };
968        let response = self.client.post(&url).json(&request).send().await?;
969        self.handle_response(response).await
970    }
971
972    // ====================================================================
973    // Decay Engine Management (DECAY-1 / DECAY-2)
974    // ====================================================================
975
976    /// Get current decay engine configuration (DECAY-1).
977    ///
978    /// Returns the active strategy, half-life, and minimum importance threshold.
979    /// Requires Admin scope.
980    pub async fn decay_config(&self) -> Result<DecayConfigResponse> {
981        let url = format!("{}/v1/admin/decay/config", self.base_url);
982        let response = self.client.get(&url).send().await?;
983        self.handle_response(response).await
984    }
985
986    /// Update decay engine configuration at runtime (DECAY-1).
987    ///
988    /// Changes take effect on the next decay cycle — no restart required.
989    /// All fields are optional; omit any to keep its current value.
990    /// Requires Admin scope.
991    pub async fn decay_update_config(
992        &self,
993        request: DecayConfigUpdateRequest,
994    ) -> Result<DecayConfigUpdateResponse> {
995        let url = format!("{}/v1/admin/decay/config", self.base_url);
996        let response = self.client.put(&url).json(&request).send().await?;
997        self.handle_response(response).await
998    }
999
1000    /// Get decay activity counters and last-cycle snapshot (DECAY-2).
1001    ///
1002    /// Returns cumulative totals (memories decayed/deleted, cycles run) and
1003    /// per-cycle statistics from the most recent run. Requires Admin scope.
1004    pub async fn decay_stats(&self) -> Result<DecayStatsResponse> {
1005        let url = format!("{}/v1/admin/decay/stats", self.base_url);
1006        let response = self.client.get(&url).send().await?;
1007        self.handle_response(response).await
1008    }
1009
1010    // ====================================================================
1011    // Product KPI Snapshot (OBS-2)
1012    // ====================================================================
1013
1014    /// Return a point-in-time product KPI snapshot (OBS-2).
1015    ///
1016    /// Calls `GET /v1/kpis`. Returns 8 operational metrics covering latency,
1017    /// error rate, and retention. Sub-millisecond — served from in-memory
1018    /// counters. Requires Admin scope.
1019    pub async fn get_kpis(&self) -> Result<KpiSnapshot> {
1020        let url = format!("{}/kpis", self.base_url);
1021        let response = self.client.get(&url).send().await?;
1022        self.handle_response(response).await
1023    }
1024
1025    // ========================================================================
1026    // CE-54: Fulltext Reindex
1027    // ========================================================================
1028
1029    /// Backfill the BM25 fulltext index for memories stored before CE-12 auto-indexing (CE-54).
1030    ///
1031    /// Calls `POST /v1/admin/fulltext/reindex`. Requires Admin scope.
1032    ///
1033    /// Scans all memories in `namespace` (or every agent namespace when `None`) and adds
1034    /// any missing from the BM25 index. Safe to call multiple times — already-indexed
1035    /// memories are counted in `total_skipped` and not re-processed.
1036    pub async fn admin_fulltext_reindex(
1037        &self,
1038        namespace: Option<&str>,
1039    ) -> Result<FulltextReindexResponse> {
1040        let url = format!("{}/v1/admin/fulltext/reindex", self.base_url);
1041        let body = serde_json::json!({ "namespace": namespace });
1042        let response = self.client.post(&url).json(&body).send().await?;
1043        self.handle_response(response).await
1044    }
1045
1046    // =========================================================================
1047    // Cluster & Maintenance
1048    // =========================================================================
1049
1050    /// GET /v1/admin/cluster/replication — cluster replication status.
1051    pub async fn admin_cluster_replication(&self) -> Result<crate::types::ReplicationStatus> {
1052        let url = format!("{}/v1/admin/cluster/replication", self.base_url);
1053        let response = self.client.get(&url).send().await?;
1054        self.handle_response(response).await
1055    }
1056
1057    /// GET /v1/admin/cluster/shards — list shards.
1058    pub async fn admin_list_shards(&self) -> Result<crate::types::ShardListResponse> {
1059        let url = format!("{}/v1/admin/cluster/shards", self.base_url);
1060        let response = self.client.get(&url).send().await?;
1061        self.handle_response(response).await
1062    }
1063
1064    /// POST /v1/admin/cluster/shards/rebalance — rebalance shards.
1065    pub async fn admin_rebalance_shards(
1066        &self,
1067        request: crate::types::ShardRebalanceRequest,
1068    ) -> Result<crate::types::ShardRebalanceResponse> {
1069        let url = format!("{}/v1/admin/cluster/shards/rebalance", self.base_url);
1070        let response = self.client.post(&url).json(&request).send().await?;
1071        self.handle_response(response).await
1072    }
1073
1074    /// GET /v1/admin/cluster/maintenance — maintenance mode status.
1075    pub async fn admin_maintenance_status(&self) -> Result<crate::types::MaintenanceStatus> {
1076        let url = format!("{}/v1/admin/cluster/maintenance", self.base_url);
1077        let response = self.client.get(&url).send().await?;
1078        self.handle_response(response).await
1079    }
1080
1081    /// POST /v1/admin/cluster/maintenance/enable — enable maintenance mode.
1082    pub async fn admin_enable_maintenance(
1083        &self,
1084        request: crate::types::EnableMaintenanceRequest,
1085    ) -> Result<crate::types::MaintenanceStatus> {
1086        let url = format!("{}/v1/admin/cluster/maintenance/enable", self.base_url);
1087        let response = self.client.post(&url).json(&request).send().await?;
1088        self.handle_response(response).await
1089    }
1090
1091    /// POST /v1/admin/cluster/maintenance/disable — disable maintenance mode.
1092    pub async fn admin_disable_maintenance(
1093        &self,
1094        request: crate::types::DisableMaintenanceRequest,
1095    ) -> Result<crate::types::MaintenanceStatus> {
1096        let url = format!("{}/v1/admin/cluster/maintenance/disable", self.base_url);
1097        let response = self.client.post(&url).json(&request).send().await?;
1098        self.handle_response(response).await
1099    }
1100
1101    // =========================================================================
1102    // Quotas
1103    // =========================================================================
1104
1105    /// GET /v1/admin/quotas — list all namespace quotas.
1106    pub async fn admin_list_quotas(&self) -> Result<crate::types::QuotaListResponse> {
1107        let url = format!("{}/v1/admin/quotas", self.base_url);
1108        let response = self.client.get(&url).send().await?;
1109        self.handle_response(response).await
1110    }
1111
1112    /// GET /v1/admin/quotas/default — get default quota configuration.
1113    pub async fn admin_get_default_quota(&self) -> Result<crate::types::DefaultQuotaResponse> {
1114        let url = format!("{}/v1/admin/quotas/default", self.base_url);
1115        let response = self.client.get(&url).send().await?;
1116        self.handle_response(response).await
1117    }
1118
1119    /// PUT /v1/admin/quotas/default — set default quota configuration.
1120    pub async fn admin_set_default_quota(
1121        &self,
1122        request: crate::types::SetDefaultQuotaRequest,
1123    ) -> Result<crate::types::SetQuotaResponse> {
1124        let url = format!("{}/v1/admin/quotas/default", self.base_url);
1125        let response = self.client.put(&url).json(&request).send().await?;
1126        self.handle_response(response).await
1127    }
1128
1129    /// GET /v1/admin/quotas/{namespace} — get namespace quota.
1130    pub async fn admin_get_quota(&self, namespace: &str) -> Result<crate::types::QuotaStatus> {
1131        let url = format!("{}/v1/admin/quotas/{}", self.base_url, namespace);
1132        let response = self.client.get(&url).send().await?;
1133        self.handle_response(response).await
1134    }
1135
1136    /// PUT /v1/admin/quotas/{namespace} — set namespace quota.
1137    pub async fn admin_set_quota(
1138        &self,
1139        namespace: &str,
1140        request: crate::types::SetQuotaRequest,
1141    ) -> Result<crate::types::SetQuotaResponse> {
1142        let url = format!("{}/v1/admin/quotas/{}", self.base_url, namespace);
1143        let response = self.client.put(&url).json(&request).send().await?;
1144        self.handle_response(response).await
1145    }
1146
1147    /// DELETE /v1/admin/quotas/{namespace} — remove namespace quota.
1148    pub async fn admin_delete_quota(&self, namespace: &str) -> Result<serde_json::Value> {
1149        let url = format!("{}/v1/admin/quotas/{}", self.base_url, namespace);
1150        let response = self.client.delete(&url).send().await?;
1151        self.handle_response(response).await
1152    }
1153
1154    /// POST /v1/admin/quotas/{namespace}/check — check if operation would exceed quota.
1155    pub async fn admin_check_quota(
1156        &self,
1157        namespace: &str,
1158        request: crate::types::QuotaCheckRequest,
1159    ) -> Result<crate::types::QuotaCheckResult> {
1160        let url = format!("{}/v1/admin/quotas/{}/check", self.base_url, namespace);
1161        let response = self.client.post(&url).json(&request).send().await?;
1162        self.handle_response(response).await
1163    }
1164
1165    // =========================================================================
1166    // Slow Queries
1167    // =========================================================================
1168
1169    /// GET /v1/admin/slow-queries — list recent slow queries.
1170    pub async fn admin_list_slow_queries(
1171        &self,
1172        namespace: Option<&str>,
1173        query_type: Option<&str>,
1174        limit: Option<u32>,
1175    ) -> Result<Vec<serde_json::Value>> {
1176        let mut url = format!("{}/v1/admin/slow-queries", self.base_url);
1177        let mut params = Vec::new();
1178        if let Some(ns) = namespace {
1179            params.push(format!("namespace={}", ns));
1180        }
1181        if let Some(qt) = query_type {
1182            params.push(format!("query_type={}", qt));
1183        }
1184        if let Some(l) = limit {
1185            params.push(format!("limit={}", l));
1186        }
1187        if !params.is_empty() {
1188            url.push('?');
1189            url.push_str(&params.join("&"));
1190        }
1191        let response = self.client.get(&url).send().await?;
1192        self.handle_response(response).await
1193    }
1194
1195    /// GET /v1/admin/slow-queries/summary — slow query summary.
1196    pub async fn admin_slow_query_summary(&self) -> Result<serde_json::Value> {
1197        let url = format!("{}/v1/admin/slow-queries/summary", self.base_url);
1198        let response = self.client.get(&url).send().await?;
1199        self.handle_response(response).await
1200    }
1201
1202    /// DELETE /v1/admin/slow-queries — clear slow query log.
1203    pub async fn admin_clear_slow_queries(
1204        &self,
1205        namespace: Option<&str>,
1206    ) -> Result<serde_json::Value> {
1207        let mut url = format!("{}/v1/admin/slow-queries", self.base_url);
1208        if let Some(ns) = namespace {
1209            url.push_str(&format!("?namespace={}", ns));
1210        }
1211        let response = self.client.delete(&url).send().await?;
1212        self.handle_response(response).await
1213    }
1214
1215    /// PATCH /v1/admin/slow-queries/config — update slow query configuration.
1216    pub async fn admin_update_slow_query_config(
1217        &self,
1218        config: serde_json::Value,
1219    ) -> Result<serde_json::Value> {
1220        let url = format!("{}/v1/admin/slow-queries/config", self.base_url);
1221        let response = self.client.patch(&url).json(&config).send().await?;
1222        self.handle_response(response).await
1223    }
1224
1225    // =========================================================================
1226    // Backups
1227    // =========================================================================
1228
1229    /// GET /v1/admin/backups — list all backups.
1230    pub async fn admin_list_backups(&self) -> Result<crate::types::BackupListResponse> {
1231        let url = format!("{}/v1/admin/backups", self.base_url);
1232        let response = self.client.get(&url).send().await?;
1233        self.handle_response(response).await
1234    }
1235
1236    /// POST /v1/admin/backups — create a new backup.
1237    pub async fn admin_create_backup(
1238        &self,
1239        request: crate::types::CreateBackupRequest,
1240    ) -> Result<crate::types::CreateBackupResponse> {
1241        let url = format!("{}/v1/admin/backups", self.base_url);
1242        let response = self.client.post(&url).json(&request).send().await?;
1243        self.handle_response(response).await
1244    }
1245
1246    /// GET /v1/admin/backups/{id} — get backup details.
1247    pub async fn admin_get_backup(&self, backup_id: &str) -> Result<crate::types::AdminBackupInfo> {
1248        let url = format!("{}/v1/admin/backups/{}", self.base_url, backup_id);
1249        let response = self.client.get(&url).send().await?;
1250        self.handle_response(response).await
1251    }
1252
1253    /// DELETE /v1/admin/backups/{id} — delete a backup.
1254    pub async fn admin_delete_backup(&self, backup_id: &str) -> Result<serde_json::Value> {
1255        let url = format!("{}/v1/admin/backups/{}", self.base_url, backup_id);
1256        let response = self.client.delete(&url).send().await?;
1257        self.handle_response(response).await
1258    }
1259
1260    /// GET /v1/admin/backups/schedule — get backup schedule.
1261    pub async fn admin_get_backup_schedule(&self) -> Result<crate::types::BackupSchedule> {
1262        let url = format!("{}/v1/admin/backups/schedule", self.base_url);
1263        let response = self.client.get(&url).send().await?;
1264        self.handle_response(response).await
1265    }
1266
1267    /// POST /v1/admin/backups/schedule — update backup schedule.
1268    pub async fn admin_update_backup_schedule(
1269        &self,
1270        request: crate::types::UpdateBackupScheduleRequest,
1271    ) -> Result<crate::types::BackupSchedule> {
1272        let url = format!("{}/v1/admin/backups/schedule", self.base_url);
1273        let response = self.client.post(&url).json(&request).send().await?;
1274        self.handle_response(response).await
1275    }
1276
1277    /// POST /v1/admin/backups/restore — restore from backup.
1278    pub async fn admin_restore_backup(
1279        &self,
1280        request: crate::types::RestoreBackupRequest,
1281    ) -> Result<crate::types::RestoreBackupResponse> {
1282        let url = format!("{}/v1/admin/backups/restore", self.base_url);
1283        let response = self.client.post(&url).json(&request).send().await?;
1284        self.handle_response(response).await
1285    }
1286
1287    /// GET /v1/admin/backups/restore/{id} — restore operation status.
1288    pub async fn admin_get_restore_status(
1289        &self,
1290        restore_id: &str,
1291    ) -> Result<crate::types::RestoreBackupResponse> {
1292        let url = format!("{}/v1/admin/backups/restore/{}", self.base_url, restore_id);
1293        let response = self.client.get(&url).send().await?;
1294        self.handle_response(response).await
1295    }
1296
1297    // =========================================================================
1298    // Ops — Diagnostics & Jobs
1299    // =========================================================================
1300
1301    /// GET /ops/diagnostics — system diagnostics.
1302    pub async fn ops_diagnostics(&self) -> Result<serde_json::Value> {
1303        let url = format!("{}/ops/diagnostics", self.base_url);
1304        let response = self.client.get(&url).send().await?;
1305        self.handle_response(response).await
1306    }
1307
1308    /// GET /ops/jobs — list background jobs.
1309    pub async fn ops_list_jobs(&self) -> Result<Vec<crate::types::JobInfo>> {
1310        let url = format!("{}/ops/jobs", self.base_url);
1311        let response = self.client.get(&url).send().await?;
1312        self.handle_response(response).await
1313    }
1314
1315    /// GET /ops/jobs/{id} — get job status.
1316    pub async fn ops_get_job(&self, job_id: &str) -> Result<crate::types::JobInfo> {
1317        let url = format!("{}/ops/jobs/{}", self.base_url, job_id);
1318        let response = self.client.get(&url).send().await?;
1319        self.handle_response(response).await
1320    }
1321
1322    /// POST /ops/compact — trigger compaction.
1323    pub async fn ops_compact(
1324        &self,
1325        request: crate::types::CompactionRequest,
1326    ) -> Result<crate::types::CompactionResponse> {
1327        let url = format!("{}/ops/compact", self.base_url);
1328        let response = self.client.post(&url).json(&request).send().await?;
1329        self.handle_response(response).await
1330    }
1331
1332    /// POST /ops/shutdown — request graceful shutdown.
1333    pub async fn ops_shutdown(&self) -> Result<serde_json::Value> {
1334        let url = format!("{}/ops/shutdown", self.base_url);
1335        let response = self.client.post(&url).send().await?;
1336        self.handle_response(response).await
1337    }
1338
1339    // =========================================================================
1340    // Backup Download / Upload
1341    // =========================================================================
1342
1343    /// Download a backup as gzipped bytes via `GET /v1/admin/backups/{id}/download`.
1344    pub async fn download_backup(&self, backup_id: &str) -> Result<Vec<u8>> {
1345        let url = format!("{}/v1/admin/backups/{}/download", self.base_url, backup_id);
1346        let response = self.client.get(&url).send().await?;
1347        if !response.status().is_success() {
1348            let status = response.status();
1349            let body = response.text().await.unwrap_or_default();
1350            return Err(crate::error::ClientError::Server {
1351                status: status.as_u16(),
1352                message: body,
1353                code: None,
1354            });
1355        }
1356        Ok(response.bytes().await?.to_vec())
1357    }
1358
1359    /// Upload a backup from gzipped bytes via `POST /v1/admin/backups/upload`.
1360    pub async fn upload_backup(&self, data: Vec<u8>) -> Result<crate::types::CreateBackupResponse> {
1361        let url = format!("{}/v1/admin/backups/upload", self.base_url);
1362        let response = self
1363            .client
1364            .post(&url)
1365            .header("Content-Type", "application/gzip")
1366            .body(data)
1367            .send()
1368            .await?;
1369        self.handle_response(response).await
1370    }
1371
1372    // =========================================================================
1373    // Storage Tier Overview
1374    // =========================================================================
1375
1376    /// Get storage tier overview via `GET /v1/admin/storage/tiers`.
1377    pub async fn storage_tier_overview(&self) -> Result<crate::types::StorageTierOverview> {
1378        let url = format!("{}/v1/admin/storage/tiers", self.base_url);
1379        let response = self.client.get(&url).send().await?;
1380        self.handle_response(response).await
1381    }
1382
1383    // =========================================================================
1384    // Background Activity
1385    // =========================================================================
1386
1387    /// Get background activity metrics via `GET /v1/admin/background-activity`.
1388    pub async fn background_activity(&self) -> Result<serde_json::Value> {
1389        let url = format!("{}/v1/admin/background-activity", self.base_url);
1390        let response = self.client.get(&url).send().await?;
1391        self.handle_response(response).await
1392    }
1393
1394    // =========================================================================
1395    // Memory Type Stats
1396    // =========================================================================
1397
1398    /// Get per-type memory statistics via `GET /v1/admin/memory-type-stats`.
1399    pub async fn memory_type_stats(&self) -> Result<crate::types::MemoryTypeStatsResponse> {
1400        let url = format!("{}/v1/admin/memory-type-stats", self.base_url);
1401        let response = self.client.get(&url).send().await?;
1402        self.handle_response(response).await
1403    }
1404
1405    // =========================================================================
1406    // Migrate Namespace Dimensions
1407    // =========================================================================
1408
1409    /// Migrate namespace embedding dimensions via `POST /v1/admin/namespaces/migrate-dimensions`.
1410    pub async fn migrate_namespace_dimensions(
1411        &self,
1412        request: crate::types::MigrateNamespaceDimensionsRequest,
1413    ) -> Result<crate::types::MigrateDimensionsResponse> {
1414        let url = format!("{}/v1/admin/namespaces/migrate-dimensions", self.base_url);
1415        let response = self.client.post(&url).json(&request).send().await?;
1416        self.handle_response(response).await
1417    }
1418
1419    // =========================================================================
1420    // ReembedJob Force-Drain (v0.11.82+)
1421    // =========================================================================
1422
1423    /// Synchronously drain all static vectors to full ONNX quality via
1424    /// `POST /v1/admin/reembed/drain` (v0.11.82+).
1425    ///
1426    /// Runs the re-embedding upgrade loop until zero `_embedding_kind=static`
1427    /// candidates remain across all namespaces, or `request.timeout_secs` elapses.
1428    /// Requires Admin scope. Useful as a pre-benchmark steady-state gate when
1429    /// `DAKERA_TIERED=1`.
1430    ///
1431    /// A [`DrainReembedResponse::remaining`] of `0` guarantees all vectors are at
1432    /// full ONNX quality.
1433    pub async fn drain_reembed(
1434        &self,
1435        request: crate::types::DrainReembedRequest,
1436    ) -> Result<crate::types::DrainReembedResponse> {
1437        let url = format!("{}/v1/admin/reembed/drain", self.base_url);
1438        let response = self.client.post(&url).json(&request).send().await?;
1439        self.handle_response(response).await
1440    }
1441
1442    /// Return the count of static vectors pending re-embedding via
1443    /// `GET /v1/admin/reembed/static-count` (v0.11.91+).
1444    ///
1445    /// Operators can poll this alongside [`drain_reembed`][Self::drain_reembed]
1446    /// to monitor drain progress. A [`StaticCountResponse::static_count`] of `0`
1447    /// means steady state — all vectors are at full ONNX quality.
1448    ///
1449    /// Requires Admin scope.
1450    pub async fn admin_reembed_static_count(&self) -> Result<crate::types::StaticCountResponse> {
1451        let url = format!("{}/v1/admin/reembed/static-count", self.base_url);
1452        let response = self.client.get(&url).send().await?;
1453        self.handle_response(response).await
1454    }
1455}
1456
1457// ============================================================================
1458// Product KPI Snapshot (OBS-2)
1459// ============================================================================
1460
1461/// Point-in-time product KPI snapshot returned by `GET /v1/kpis` (OBS-2).
1462///
1463/// All latency values are in milliseconds. Rate/percentage values are in the
1464/// range `0.0`–`100.0`. Integer counts are unsigned.
1465///
1466/// Requires Admin scope.
1467#[derive(Debug, Clone, Serialize, Deserialize)]
1468pub struct KpiSnapshot {
1469    /// Median recall latency across all namespaces over the last minute (ms).
1470    pub recall_latency_p50_ms: f64,
1471    /// 99th-percentile recall latency across all namespaces over the last minute (ms).
1472    pub recall_latency_p99_ms: f64,
1473    /// Median store latency across all namespaces over the last minute (ms).
1474    pub store_latency_p50_ms: f64,
1475    /// 5xx error rate as a percentage of total API requests over the last minute.
1476    pub api_error_rate_5xx_pct: f64,
1477    /// Distinct agent identifiers that stored or recalled a memory in the last 24 hours.
1478    pub active_agents_count: u64,
1479    /// Total sessions created in the rolling 7-day window.
1480    pub session_count_week: u64,
1481    /// Current number of nodes in the cross-agent knowledge graph.
1482    pub cross_agent_network_node_count: u64,
1483    /// Percentage of memories created 7 days ago that are still active.
1484    pub memory_retention_7d_pct: f64,
1485}
1486
1487// ============================================================================
1488// CE-54: Fulltext Reindex (Admin)
1489// ============================================================================
1490
1491/// Per-namespace result from `POST /v1/admin/fulltext/reindex` (CE-54).
1492#[derive(Debug, Clone, Serialize, Deserialize)]
1493pub struct FulltextReindexNamespaceResult {
1494    /// Namespace that was scanned.
1495    pub namespace: String,
1496    /// Total vectors examined.
1497    pub vectors_scanned: usize,
1498    /// Memories newly added to the BM25 index.
1499    pub newly_indexed: usize,
1500    /// Memories already in the BM25 index (skipped).
1501    pub already_indexed: usize,
1502    /// Memories that could not be parsed.
1503    pub parse_failures: usize,
1504}
1505
1506/// Response from `POST /v1/admin/fulltext/reindex` (CE-54).
1507///
1508/// Returned by [`DakeraClient::admin_fulltext_reindex`].
1509#[derive(Debug, Clone, Serialize, Deserialize)]
1510pub struct FulltextReindexResponse {
1511    /// Number of namespaces scanned.
1512    pub namespaces_processed: usize,
1513    /// Total memories newly added to BM25 across all namespaces.
1514    pub total_indexed: usize,
1515    /// Total memories already in the BM25 index (skipped).
1516    pub total_skipped: usize,
1517    /// Per-namespace breakdown.
1518    pub details: Vec<FulltextReindexNamespaceResult>,
1519}