Skip to content

Commit 2472299

Browse files
committed
feat: implement Phases 20-24 — scalability, AI intelligence, voice collab, growth, sustainability
1 parent 17d3cc3 commit 2472299

39 files changed

Lines changed: 7646 additions & 98 deletions

.planning/ROADMAP.md

Lines changed: 97 additions & 97 deletions
Large diffs are not rendered by default.

crates/nexus-api/src/lib.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,16 @@ pub fn build_router(state: AppState) -> Router {
129129
.merge(routes::onboarding::router()) // onboarding wizard & templates
130130
.merge(routes::admin_analytics::router()) // admin analytics snapshots
131131
.merge(routes::marketplace::router()) // plugin marketplace
132+
// v1.9 Scalability & Performance Hardening
133+
.merge(routes::scalability::router()) // scaling configs, SFU, federation batches, voice quality
134+
// v2.0 AI & Intelligence Layer
135+
.merge(routes::ai_layer::router()) // AI suggestions, toxicity, raid detection, consent
136+
// v2.1 Voice & Real-Time Collaboration
137+
.merge(routes::voice_collab::router()) // video layouts, live streams, breakout rooms, spatial audio
138+
// v2.2 User Growth & Retention
139+
.merge(routes::growth::router()) // recommendations, gamification, achievements, offline queue
140+
// v2.x Sustainability & Extensibility
141+
.merge(routes::sustainability::router()) // governance, protocol versions, security audits, tutorials
132142
// v0.8.5 Federation UX — admin management + cross-instance search
133143
.merge(routes::federation_admin::router()) // federation admin panel API
134144
// Make Arc<AppState> available as an Axum Extension so that
Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
//! AI & intelligence layer routes — Phase 21.
2+
//!
3+
//! Search embeddings, AI suggestions, thread summaries, toxicity,
4+
//! raid detection, voice transcripts, consent & audit.
5+
6+
use axum::{
7+
extract::{Extension, Path, Query, State},
8+
middleware,
9+
routing::get,
10+
Json, Router,
11+
};
12+
use nexus_common::error::{NexusError, NexusResult};
13+
use nexus_common::models::ai_intelligence::*;
14+
use nexus_db::repository::{ai_intelligence, members};
15+
use nexus_common::models::Member;
16+
use serde::Deserialize;
17+
use std::sync::Arc;
18+
use uuid::Uuid;
19+
20+
use crate::{middleware::AuthContext, AppState};
21+
22+
// ── Router ─────────────────────────────────────────────────────────────────
23+
24+
pub fn router() -> Router<Arc<AppState>> {
25+
Router::new()
26+
// AI suggestions
27+
.route(
28+
"/channels/:channel_id/ai-suggestions",
29+
get(list_ai_suggestions).post(create_ai_suggestion),
30+
)
31+
// Thread summaries
32+
.route(
33+
"/channels/:channel_id/thread-summary",
34+
get(get_thread_summary).post(upsert_thread_summary),
35+
)
36+
// Toxicity
37+
.route(
38+
"/servers/:server_id/toxicity-flags",
39+
get(list_flagged_toxicity),
40+
)
41+
// Raid detection
42+
.route("/servers/:server_id/raids", get(list_raid_detections))
43+
// Voice transcripts
44+
.route(
45+
"/channels/:channel_id/voice-transcripts",
46+
get(list_voice_transcripts),
47+
)
48+
// AI consent (per server)
49+
.route(
50+
"/servers/:server_id/ai-consent",
51+
get(list_ai_consent).post(upsert_ai_consent),
52+
)
53+
// AI audit log (per server)
54+
.route("/servers/:server_id/ai-audit-log", get(list_ai_audit_log))
55+
.route_layer(middleware::from_fn(crate::middleware::combined_auth_middleware))
56+
}
57+
58+
// ── Request / Query ────────────────────────────────────────────────────────
59+
60+
#[derive(Debug, Deserialize)]
61+
struct LimitQuery { limit: Option<i64> }
62+
63+
#[derive(Debug, Deserialize)]
64+
struct CreateAiSuggestionReq {
65+
suggestion_type: String,
66+
content: String,
67+
context_ids: Option<serde_json::Value>,
68+
model_name: Option<String>,
69+
}
70+
71+
#[derive(Debug, Deserialize)]
72+
struct UpsertThreadSummaryReq {
73+
thread_id: Uuid,
74+
summary: String,
75+
message_count: Option<i32>,
76+
model_name: Option<String>,
77+
model_version: Option<String>,
78+
}
79+
80+
#[derive(Debug, Deserialize)]
81+
struct UpsertAiConsentReq {
82+
feature: String,
83+
enabled: bool,
84+
}
85+
86+
// ── Handlers ──────────────────────────────────────────────────────────────
87+
88+
async fn list_ai_suggestions(
89+
State(state): State<Arc<AppState>>,
90+
Extension(ctx): Extension<AuthContext>,
91+
Path(channel_id): Path<Uuid>,
92+
Query(q): Query<LimitQuery>,
93+
) -> NexusResult<Json<Vec<AiSuggestion>>> {
94+
let rows = ai_intelligence::list_ai_suggestions(
95+
&state.db.pool, ctx.user_id, channel_id, q.limit.unwrap_or(50),
96+
).await.map_err(|e| NexusError::Internal(e.into()))?;
97+
Ok(Json(rows))
98+
}
99+
100+
async fn create_ai_suggestion(
101+
State(state): State<Arc<AppState>>,
102+
Extension(ctx): Extension<AuthContext>,
103+
Path(channel_id): Path<Uuid>,
104+
Json(body): Json<CreateAiSuggestionReq>,
105+
) -> NexusResult<Json<AiSuggestion>> {
106+
let row = ai_intelligence::create_ai_suggestion(
107+
&state.db.pool, Uuid::new_v4(), ctx.user_id, channel_id,
108+
&body.suggestion_type, &body.content,
109+
&body.context_ids.unwrap_or(serde_json::json!([])),
110+
&body.model_name.unwrap_or_else(|| "default".into()),
111+
).await.map_err(|e| NexusError::Internal(e.into()))?;
112+
Ok(Json(row))
113+
}
114+
115+
async fn get_thread_summary(
116+
State(state): State<Arc<AppState>>,
117+
Extension(_ctx): Extension<AuthContext>,
118+
Path(channel_id): Path<Uuid>,
119+
) -> NexusResult<Json<Option<ThreadSummary>>> {
120+
// channel_id is used as thread_id for lookup
121+
let row = ai_intelligence::get_thread_summary(&state.db.pool, channel_id)
122+
.await.map_err(|e| NexusError::Internal(e.into()))?;
123+
Ok(Json(row))
124+
}
125+
126+
async fn upsert_thread_summary(
127+
State(state): State<Arc<AppState>>,
128+
Extension(_ctx): Extension<AuthContext>,
129+
Path(channel_id): Path<Uuid>,
130+
Json(body): Json<UpsertThreadSummaryReq>,
131+
) -> NexusResult<Json<ThreadSummary>> {
132+
let row = ai_intelligence::upsert_thread_summary(
133+
&state.db.pool, Uuid::new_v4(), body.thread_id, channel_id,
134+
&body.summary, body.message_count.unwrap_or(0),
135+
&body.model_name.unwrap_or_else(|| "default".into()),
136+
&body.model_version.unwrap_or_else(|| "1.0".into()),
137+
).await.map_err(|e| NexusError::Internal(e.into()))?;
138+
Ok(Json(row))
139+
}
140+
141+
async fn list_flagged_toxicity(
142+
State(state): State<Arc<AppState>>,
143+
Extension(ctx): Extension<AuthContext>,
144+
Path(server_id): Path<Uuid>,
145+
Query(q): Query<LimitQuery>,
146+
) -> NexusResult<Json<Vec<ToxicityScore>>> {
147+
let _m: Option<Member> = members::find_member(&state.db.pool, ctx.user_id, server_id)
148+
.await.map_err(|e| NexusError::Internal(e.into()))?;
149+
if _m.is_none() { return Err(NexusError::Forbidden); }
150+
151+
let rows = ai_intelligence::list_flagged_toxicity(&state.db.pool, server_id, q.limit.unwrap_or(100))
152+
.await.map_err(|e| NexusError::Internal(e.into()))?;
153+
Ok(Json(rows))
154+
}
155+
156+
async fn list_raid_detections(
157+
State(state): State<Arc<AppState>>,
158+
Extension(ctx): Extension<AuthContext>,
159+
Path(server_id): Path<Uuid>,
160+
Query(q): Query<LimitQuery>,
161+
) -> NexusResult<Json<Vec<RaidDetection>>> {
162+
let _m: Option<Member> = members::find_member(&state.db.pool, ctx.user_id, server_id)
163+
.await.map_err(|e| NexusError::Internal(e.into()))?;
164+
if _m.is_none() { return Err(NexusError::Forbidden); }
165+
166+
let rows = ai_intelligence::list_raid_detections(&state.db.pool, server_id, q.limit.unwrap_or(50))
167+
.await.map_err(|e| NexusError::Internal(e.into()))?;
168+
Ok(Json(rows))
169+
}
170+
171+
async fn list_voice_transcripts(
172+
State(state): State<Arc<AppState>>,
173+
Extension(_ctx): Extension<AuthContext>,
174+
Path(channel_id): Path<Uuid>,
175+
Query(q): Query<LimitQuery>,
176+
) -> NexusResult<Json<Vec<VoiceTranscript>>> {
177+
let rows = ai_intelligence::list_voice_transcripts(&state.db.pool, channel_id, q.limit.unwrap_or(100))
178+
.await.map_err(|e| NexusError::Internal(e.into()))?;
179+
Ok(Json(rows))
180+
}
181+
182+
async fn list_ai_consent(
183+
State(state): State<Arc<AppState>>,
184+
Extension(ctx): Extension<AuthContext>,
185+
Path(server_id): Path<Uuid>,
186+
) -> NexusResult<Json<Vec<AiConsent>>> {
187+
let rows = ai_intelligence::list_ai_consent(&state.db.pool, ctx.user_id, server_id)
188+
.await.map_err(|e| NexusError::Internal(e.into()))?;
189+
Ok(Json(rows))
190+
}
191+
192+
async fn upsert_ai_consent(
193+
State(state): State<Arc<AppState>>,
194+
Extension(ctx): Extension<AuthContext>,
195+
Path(server_id): Path<Uuid>,
196+
Json(body): Json<UpsertAiConsentReq>,
197+
) -> NexusResult<Json<AiConsent>> {
198+
let row = ai_intelligence::upsert_ai_consent(
199+
&state.db.pool, ctx.user_id, server_id, &body.feature, body.enabled,
200+
).await.map_err(|e| NexusError::Internal(e.into()))?;
201+
Ok(Json(row))
202+
}
203+
204+
async fn list_ai_audit_log(
205+
State(state): State<Arc<AppState>>,
206+
Extension(ctx): Extension<AuthContext>,
207+
Path(server_id): Path<Uuid>,
208+
Query(q): Query<LimitQuery>,
209+
) -> NexusResult<Json<Vec<AiAuditEntry>>> {
210+
let _m: Option<Member> = members::find_member(&state.db.pool, ctx.user_id, server_id)
211+
.await.map_err(|e| NexusError::Internal(e.into()))?;
212+
if _m.is_none() { return Err(NexusError::Forbidden); }
213+
214+
let rows = ai_intelligence::list_ai_audit_log(&state.db.pool, server_id, q.limit.unwrap_or(200))
215+
.await.map_err(|e| NexusError::Internal(e.into()))?;
216+
Ok(Json(rows))
217+
}

0 commit comments

Comments
 (0)