mockforge_ui/
routes.rs

1//! Route definitions for the admin UI
2
3use axum::{
4    routing::{delete, get, post},
5    Router,
6};
7use tower_http::{compression::CompressionLayer, cors::CorsLayer};
8
9use crate::audit::init_global_audit_store;
10use crate::auth::init_global_user_store;
11use crate::handlers::analytics::AnalyticsState;
12use crate::handlers::AdminState;
13use crate::handlers::*;
14use crate::rbac::rbac_middleware;
15use crate::time_travel_handlers;
16use axum::middleware::from_fn;
17use mockforge_core::{get_global_logger, init_global_logger};
18
19/// Create the admin router with static assets and optional API endpoints
20///
21/// # Arguments
22/// * `http_server_addr` - HTTP server address
23/// * `ws_server_addr` - WebSocket server address
24/// * `grpc_server_addr` - gRPC server address
25/// * `graphql_server_addr` - GraphQL server address
26/// * `api_enabled` - Whether API endpoints are enabled
27/// * `admin_port` - Admin server port
28/// * `prometheus_url` - Prometheus metrics URL
29/// * `chaos_api_state` - Optional chaos API state for hot-reload support
30/// * `latency_injector` - Optional latency injector for hot-reload support
31/// * `mockai` - Optional MockAI instance for hot-reload support
32/// * `continuum_config` - Optional Reality Continuum configuration
33/// * `virtual_clock` - Optional virtual clock for time-based progression
34pub fn create_admin_router(
35    http_server_addr: Option<std::net::SocketAddr>,
36    ws_server_addr: Option<std::net::SocketAddr>,
37    grpc_server_addr: Option<std::net::SocketAddr>,
38    graphql_server_addr: Option<std::net::SocketAddr>,
39    api_enabled: bool,
40    admin_port: u16,
41    prometheus_url: String,
42    chaos_api_state: Option<std::sync::Arc<mockforge_chaos::api::ChaosApiState>>,
43    latency_injector: Option<
44        std::sync::Arc<tokio::sync::RwLock<mockforge_core::latency::LatencyInjector>>,
45    >,
46    mockai: Option<
47        std::sync::Arc<tokio::sync::RwLock<mockforge_core::intelligent_behavior::MockAI>>,
48    >,
49    continuum_config: Option<mockforge_core::ContinuumConfig>,
50    virtual_clock: Option<std::sync::Arc<mockforge_core::VirtualClock>>,
51) -> Router {
52    // Initialize global logger if not already initialized
53    let _logger = get_global_logger().unwrap_or_else(|| init_global_logger(1000));
54
55    // Initialize audit log store (keep last 10000 audit entries)
56    let _audit_store = init_global_audit_store(10000);
57
58    // Initialize user store for authentication
59    let _user_store = init_global_user_store();
60
61    let state = AdminState::new(
62        http_server_addr,
63        ws_server_addr,
64        grpc_server_addr,
65        graphql_server_addr,
66        api_enabled,
67        admin_port,
68        chaos_api_state,
69        latency_injector,
70        mockai,
71        continuum_config,
72        virtual_clock,
73    );
74
75    // Start system monitoring background task to poll CPU, memory, and thread metrics
76    let state_clone = state.clone();
77    tokio::spawn(async move {
78        state_clone.start_system_monitoring().await;
79    });
80    let mut router = Router::new()
81        // Public routes (no authentication required)
82        .route("/", get(serve_admin_html))
83        .route("/assets/index.css", get(serve_admin_css))
84        .route("/assets/index.js", get(serve_admin_js))
85        .route("/assets/{filename}", get(serve_vendor_asset))
86        .route("/api-docs", get(serve_api_docs))
87        .route("/mockforge-icon.png", get(serve_icon))
88        .route("/mockforge-icon-32.png", get(serve_icon_32))
89        .route("/mockforge-icon-48.png", get(serve_icon_48))
90        .route("/mockforge-logo.png", get(serve_logo))
91        .route("/mockforge-logo-40.png", get(serve_logo_40))
92        .route("/mockforge-logo-80.png", get(serve_logo_80))
93        .route("/manifest.json", get(serve_manifest))
94        .route("/sw.js", get(serve_service_worker))
95        // Authentication endpoints (public)
96        .route("/__mockforge/auth/login", post(crate::auth::login))
97        .route("/__mockforge/auth/refresh", post(crate::auth::refresh_token))
98        .route("/__mockforge/auth/logout", post(crate::auth::logout))
99        .route("/__mockforge/health", get(get_health));
100
101    // Protected routes (require authentication and RBAC)
102    router = router
103        .route("/__mockforge/dashboard", get(get_dashboard))
104        .route("/_mf", get(get_dashboard))  // Short alias for dashboard
105        .route("/admin/server-info", get(get_server_info))
106        .route("/__mockforge/server-info", get(get_server_info))
107        .route("/__mockforge/routes", get(get_routes))
108        .route("/__mockforge/logs", get(get_logs))
109        .route("/__mockforge/logs/sse", get(logs_sse))
110        .route("/__mockforge/metrics", get(get_metrics))
111        .route("/__mockforge/api/reality/trace/{request_id}", get(get_reality_trace))
112        .route("/__mockforge/api/reality/response-trace/{request_id}", get(get_response_trace))
113        .route("/__mockforge/config", get(get_config))
114        .route("/__mockforge/config/latency", post(update_latency))
115        .route("/__mockforge/config/faults", post(update_faults))
116        .route("/__mockforge/config/proxy", post(update_proxy))
117        .route("/__mockforge/config/traffic-shaping", post(update_traffic_shaping))
118        .route("/__mockforge/logs", delete(clear_logs))
119        .route("/__mockforge/restart", post(restart_servers))
120        .route("/__mockforge/restart/status", get(get_restart_status))
121        .route("/__mockforge/fixtures", get(get_fixtures))
122        .route("/__mockforge/fixtures/{id}", delete(delete_fixture))
123        .route("/__mockforge/fixtures/bulk", delete(delete_fixtures_bulk))
124        .route("/__mockforge/audit/logs", get(get_audit_logs))
125        .route("/__mockforge/audit/stats", get(get_audit_stats))
126        .route("/__mockforge/fixtures/{id}/download", get(download_fixture))
127        .route("/__mockforge/fixtures/{id}/rename", post(rename_fixture))
128        .route("/__mockforge/fixtures/{id}/move", post(move_fixture))
129        // Import routes
130        .route("/__mockforge/import/postman", post(import_postman))
131        .route("/__mockforge/import/insomnia", post(import_insomnia))
132        .route("/__mockforge/import/curl", post(import_curl))
133        .route("/__mockforge/import/preview", post(preview_import))
134        .route("/__mockforge/import/history", get(get_import_history))
135        .route("/__mockforge/import/history/clear", post(clear_import_history))
136        // Plugin management routes
137        .route("/__mockforge/plugins", get(get_plugins))
138        .route("/__mockforge/plugins/status", get(get_plugin_status))
139        .route("/__mockforge/plugins/{id}", get(get_plugin_details))
140        .route("/__mockforge/plugins/{id}", delete(delete_plugin))
141        .route("/__mockforge/plugins/reload", post(reload_plugin))
142        // Workspace management routes (moved to workspace router with WorkspaceState)
143        // These routes are now handled by the workspace router below
144        // Chain management routes - proxy to main HTTP server
145        .route("/__mockforge/chains", get(proxy_chains_list))
146        .route("/__mockforge/chains", post(proxy_chains_create))
147        .route("/__mockforge/chains/{id}", get(proxy_chain_get))
148        .route("/__mockforge/chains/{id}", axum::routing::put(proxy_chain_update))
149        .route("/__mockforge/chains/{id}", delete(proxy_chain_delete))
150        .route("/__mockforge/chains/{id}/execute", post(proxy_chain_execute))
151        .route("/__mockforge/chains/{id}/validate", post(proxy_chain_validate))
152        .route("/__mockforge/chains/{id}/history", get(proxy_chain_history))
153        // Graph visualization routes
154        .route("/__mockforge/graph", get(get_graph))
155        .route("/__mockforge/graph/sse", get(graph_sse))
156        // Validation configuration routes
157        .route("/__mockforge/validation", get(get_validation))
158        .route("/__mockforge/validation", post(update_validation))
159        // Migration pipeline routes
160        .route("/__mockforge/migration/routes", get(migration::get_migration_routes))
161        .route("/__mockforge/migration/routes/{pattern}/toggle", post(migration::toggle_route_migration))
162        .route("/__mockforge/migration/routes/{pattern}", axum::routing::put(migration::set_route_migration_mode))
163        .route("/__mockforge/migration/groups/{group}/toggle", post(migration::toggle_group_migration))
164        .route("/__mockforge/migration/groups/{group}", axum::routing::put(migration::set_group_migration_mode))
165        .route("/__mockforge/migration/groups", get(migration::get_migration_groups))
166        .route("/__mockforge/migration/status", get(migration::get_migration_status))
167        // Environment variables routes
168        .route("/__mockforge/env", get(get_env_vars))
169        .route("/__mockforge/env", post(update_env_var))
170        // File management routes
171        .route("/__mockforge/files/content", post(get_file_content))
172        .route("/__mockforge/files/save", post(save_file_content))
173        // Smoke test routes
174        .route("/__mockforge/smoke", get(get_smoke_tests))
175        .route("/__mockforge/smoke/run", get(run_smoke_tests_endpoint))
176        // Time travel / temporal testing routes
177        .route("/__mockforge/time-travel/status", get(time_travel_handlers::get_time_travel_status))
178        .route("/__mockforge/time-travel/enable", post(time_travel_handlers::enable_time_travel))
179        .route("/__mockforge/time-travel/disable", post(time_travel_handlers::disable_time_travel))
180        .route("/__mockforge/time-travel/advance", post(time_travel_handlers::advance_time))
181        .route("/__mockforge/time-travel/set", post(time_travel_handlers::set_time))
182        .route("/__mockforge/time-travel/scale", post(time_travel_handlers::set_time_scale))
183        .route("/__mockforge/time-travel/reset", post(time_travel_handlers::reset_time_travel))
184        .route("/__mockforge/time-travel/schedule", post(time_travel_handlers::schedule_response))
185        .route("/__mockforge/time-travel/scheduled", get(time_travel_handlers::list_scheduled_responses))
186        .route("/__mockforge/time-travel/scheduled/{id}", delete(time_travel_handlers::cancel_scheduled_response))
187        .route("/__mockforge/time-travel/scheduled/clear", post(time_travel_handlers::clear_scheduled_responses))
188        .route("/__mockforge/time-travel/scenario/save", post(time_travel_handlers::save_scenario))
189        .route("/__mockforge/time-travel/scenario/load", post(time_travel_handlers::load_scenario))
190        // Cron job management routes
191        .route("/__mockforge/time-travel/cron", get(time_travel_handlers::list_cron_jobs))
192        .route("/__mockforge/time-travel/cron", post(time_travel_handlers::create_cron_job))
193        .route("/__mockforge/time-travel/cron/{id}", get(time_travel_handlers::get_cron_job))
194        .route("/__mockforge/time-travel/cron/{id}", delete(time_travel_handlers::delete_cron_job))
195        .route("/__mockforge/time-travel/cron/{id}/enable", post(time_travel_handlers::set_cron_job_enabled))
196        // Mutation rule management routes
197        .route("/__mockforge/time-travel/mutations", get(time_travel_handlers::list_mutation_rules))
198        .route("/__mockforge/time-travel/mutations", post(time_travel_handlers::create_mutation_rule))
199        .route("/__mockforge/time-travel/mutations/{id}", get(time_travel_handlers::get_mutation_rule))
200        .route("/__mockforge/time-travel/mutations/{id}", delete(time_travel_handlers::delete_mutation_rule))
201        .route("/__mockforge/time-travel/mutations/{id}/enable", post(time_travel_handlers::set_mutation_rule_enabled))
202        // Verification routes
203        .route("/__mockforge/verification/verify", post(verification::verify))
204        .route("/__mockforge/verification/count", post(verification::count))
205        .route("/__mockforge/verification/sequence", post(verification::verify_sequence_handler))
206        .route("/__mockforge/verification/never", post(verification::verify_never_handler))
207        .route("/__mockforge/verification/at-least", post(verification::verify_at_least_handler))
208        // Reality Slider routes
209        .route("/__mockforge/reality/level", get(get_reality_level))
210        .route("/__mockforge/reality/level", axum::routing::put(set_reality_level))
211        .route("/__mockforge/reality/presets", get(list_reality_presets))
212        .route("/__mockforge/reality/presets/import", post(import_reality_preset))
213        .route("/__mockforge/reality/presets/export", post(export_reality_preset))
214        // Reality Continuum routes
215        .route("/__mockforge/continuum/ratio", get(get_continuum_ratio))
216        .route("/__mockforge/continuum/ratio", axum::routing::put(set_continuum_ratio))
217        .route("/__mockforge/continuum/schedule", get(get_continuum_schedule))
218        .route("/__mockforge/continuum/schedule", axum::routing::put(set_continuum_schedule))
219        .route("/__mockforge/continuum/advance", post(advance_continuum_ratio))
220        .route("/__mockforge/continuum/enabled", axum::routing::put(set_continuum_enabled))
221        .route("/__mockforge/continuum/overrides", get(get_continuum_overrides))
222        .route("/__mockforge/continuum/overrides", axum::routing::delete(clear_continuum_overrides))
223        // Contract diff routes
224        .route("/__mockforge/contract-diff/upload", post(contract_diff::upload_request))
225        .route("/__mockforge/contract-diff/submit", post(contract_diff::submit_request))
226        .route("/__mockforge/contract-diff/captures", get(contract_diff::get_captured_requests))
227        .route("/__mockforge/contract-diff/captures/{id}", get(contract_diff::get_captured_request))
228        .route("/__mockforge/contract-diff/captures/{id}/analyze", post(contract_diff::analyze_captured_request))
229        .route("/__mockforge/contract-diff/captures/{id}/patch", post(contract_diff::generate_patch_file))
230        .route("/__mockforge/contract-diff/statistics", get(contract_diff::get_capture_statistics))
231        // Playground routes
232        .route("/__mockforge/playground/endpoints", get(playground::list_playground_endpoints))
233        .route("/__mockforge/playground/execute", post(playground::execute_rest_request))
234        .route("/__mockforge/playground/graphql", post(playground::execute_graphql_query))
235        .route("/__mockforge/playground/graphql/introspect", get(playground::graphql_introspect))
236        .route("/__mockforge/playground/history", get(playground::get_request_history))
237        .route("/__mockforge/playground/history/{id}/replay", post(playground::replay_request))
238        .route("/__mockforge/playground/snippets", post(playground::generate_code_snippet))
239        // Voice + LLM Interface routes
240        .route("/api/v2/voice/process", post(voice::process_voice_command))
241        .route("/__mockforge/voice/process", post(voice::process_voice_command))
242        .route("/api/v2/voice/transpile-hook", post(voice::transpile_hook))
243        .route("/__mockforge/voice/transpile-hook", post(voice::transpile_hook))
244        .route(
245            "/api/v2/voice/create-workspace-scenario",
246            post(voice::create_workspace_scenario),
247        )
248        .route(
249            "/__mockforge/voice/create-workspace-scenario",
250            post(voice::create_workspace_scenario),
251        )
252        .route(
253            "/api/v2/voice/create-workspace-preview",
254            post(voice::create_workspace_preview),
255        )
256        .route(
257            "/__mockforge/voice/create-workspace-preview",
258            post(voice::create_workspace_preview),
259        )
260        // create-workspace-confirm route moved to workspace router with WorkspaceState
261        // AI Studio routes
262        .route("/api/v1/ai-studio/chat", post(ai_studio::chat))
263        .route("/__mockforge/ai-studio/chat", post(ai_studio::chat))
264        .route("/api/v1/ai-studio/generate-mock", post(ai_studio::generate_mock))
265        .route("/__mockforge/ai-studio/generate-mock", post(ai_studio::generate_mock))
266        .route("/api/v1/ai-studio/debug-test", post(ai_studio::debug_test))
267        .route("/__mockforge/ai-studio/debug-test", post(ai_studio::debug_test))
268        .route("/api/v1/ai-studio/debug/analyze-with-context", post(ai_studio::debug_analyze_with_context))
269        .route("/__mockforge/ai-studio/debug/analyze-with-context", post(ai_studio::debug_analyze_with_context))
270        .route("/api/v1/ai-studio/generate-persona", post(ai_studio::generate_persona))
271        .route("/__mockforge/ai-studio/generate-persona", post(ai_studio::generate_persona))
272        .route("/api/v1/ai-studio/freeze", post(ai_studio::freeze_artifact))
273        .route("/__mockforge/ai-studio/freeze", post(ai_studio::freeze_artifact))
274        .route("/api/v1/ai-studio/frozen", get(ai_studio::list_frozen))
275        .route("/__mockforge/ai-studio/frozen", get(ai_studio::list_frozen))
276        .route("/api/v1/ai-studio/apply-patch", post(ai_studio::apply_patch))
277        .route("/__mockforge/ai-studio/apply-patch", post(ai_studio::apply_patch))
278        .route("/api/v1/ai-studio/usage", get(ai_studio::get_usage))
279        .route("/__mockforge/ai-studio/usage", get(ai_studio::get_usage))
280        .route("/api/v1/ai-studio/org-controls", get(ai_studio::get_org_controls))
281        .route("/__mockforge/ai-studio/org-controls", get(ai_studio::get_org_controls))
282        .route("/api/v1/ai-studio/org-controls", axum::routing::put(ai_studio::update_org_controls))
283        .route("/__mockforge/ai-studio/org-controls", axum::routing::put(ai_studio::update_org_controls))
284        .route("/api/v1/ai-studio/org-controls/usage", get(ai_studio::get_org_usage))
285        .route("/__mockforge/ai-studio/org-controls/usage", get(ai_studio::get_org_usage))
286        .route("/api/v1/ai-studio/contract-diff/query", post(ai_studio::contract_diff_query))
287        .route("/__mockforge/ai-studio/contract-diff/query", post(ai_studio::contract_diff_query))
288        // Failure analysis routes
289        .route("/api/v2/failures/analyze", post(failure_analysis::analyze_failure))
290        .route("/api/v2/failures/{request_id}", get(failure_analysis::get_failure_analysis))
291        .route("/api/v2/failures/recent", get(failure_analysis::list_recent_failures))
292        .route("/__mockforge/failures/analyze", post(failure_analysis::analyze_failure))
293        .route("/__mockforge/failures/{request_id}", get(failure_analysis::get_failure_analysis))
294        .route("/__mockforge/failures/recent", get(failure_analysis::list_recent_failures))
295        // Community portal routes
296        .route("/__mockforge/community/showcase/projects", get(community::get_showcase_projects))
297        .route("/__mockforge/community/showcase/projects/{id}", get(community::get_showcase_project))
298        .route("/__mockforge/community/showcase/categories", get(community::get_showcase_categories))
299        .route("/__mockforge/community/showcase/stories", get(community::get_success_stories))
300        .route("/__mockforge/community/showcase/submit", post(community::submit_showcase_project))
301        .route("/__mockforge/community/learning/resources", get(community::get_learning_resources))
302        .route("/__mockforge/community/learning/resources/{id}", get(community::get_learning_resource))
303        .route("/__mockforge/community/learning/categories", get(community::get_learning_categories))
304        // Behavioral cloning / flow management routes
305        .route("/__mockforge/flows", get(behavioral_cloning::get_flows))
306        .route("/__mockforge/flows/{id}", get(behavioral_cloning::get_flow))
307        .route("/__mockforge/flows/{id}/tag", axum::routing::put(behavioral_cloning::tag_flow))
308        .route("/__mockforge/flows/{id}/compile", post(behavioral_cloning::compile_flow))
309        .route("/__mockforge/scenarios", get(behavioral_cloning::get_scenarios))
310        .route("/__mockforge/scenarios/{id}", get(behavioral_cloning::get_scenario))
311        .route("/__mockforge/scenarios/{id}/export", get(behavioral_cloning::export_scenario))
312        // Health check endpoints for Kubernetes probes
313        .route("/health/live", get(health::liveness_probe))
314        .route("/health/ready", get(health::readiness_probe))
315        .route("/health/startup", get(health::startup_probe))
316        .route("/health", get(health::deep_health_check))
317        // Kubernetes-style health endpoint aliases
318        .route("/healthz", get(health::deep_health_check))
319        .route("/readyz", get(health::readiness_probe))
320        .route("/livez", get(health::liveness_probe))
321        .route("/startupz", get(health::startup_probe));
322
323    // Analytics routes with Prometheus integration
324    let analytics_state = AnalyticsState::new(prometheus_url);
325
326    let analytics_router = Router::new()
327        .route("/__mockforge/analytics/summary", get(analytics::get_summary))
328        .route("/__mockforge/analytics/requests", get(analytics::get_requests))
329        .route("/__mockforge/analytics/endpoints", get(analytics::get_endpoints))
330        .route("/__mockforge/analytics/websocket", get(analytics::get_websocket))
331        .route("/__mockforge/analytics/smtp", get(analytics::get_smtp))
332        .route("/__mockforge/analytics/system", get(analytics::get_system))
333        .with_state(analytics_state);
334
335    router = router.merge(analytics_router);
336
337    // Coverage metrics routes (MockOps)
338    // Note: Database initialization is done lazily when routes are accessed
339    // The handlers will initialize the database connection on first use
340    {
341        use crate::handlers::coverage_metrics::CoverageMetricsState;
342        use mockforge_analytics::AnalyticsDatabase;
343        use std::path::PathBuf;
344        use std::sync::Arc;
345        use tokio::sync::OnceCell;
346
347        // Initialize database lazily in a background task
348        let db_path = std::env::var("MOCKFORGE_ANALYTICS_DB_PATH")
349            .ok()
350            .map(PathBuf::from)
351            .unwrap_or_else(|| PathBuf::from("analytics.db"));
352
353        let db_path_clone = db_path.clone();
354        let coverage_db = Arc::new(OnceCell::new());
355        let coverage_db_clone = coverage_db.clone();
356
357        // Spawn task to initialize database
358        tokio::spawn(async move {
359            match AnalyticsDatabase::new(&db_path_clone).await {
360                Ok(analytics_db) => {
361                    if let Err(e) = analytics_db.run_migrations().await {
362                        tracing::warn!("Failed to run analytics database migrations: {}. Coverage metrics routes may not work correctly.", e);
363                    } else {
364                        let _ = coverage_db_clone.set(analytics_db);
365                        tracing::info!("Analytics database initialized for coverage metrics");
366                    }
367                }
368                Err(e) => {
369                    tracing::debug!("Failed to initialize analytics database for coverage metrics: {}. Coverage metrics routes will be unavailable.", e);
370                }
371            }
372        });
373
374        // Create state with lazy database initialization
375        let coverage_state = CoverageMetricsState { db: coverage_db };
376
377        // Add routes directly to main router to avoid state type conflicts
378        use crate::handlers::coverage_metrics;
379        router = router
380            .route("/api/v2/analytics/scenarios/usage", get(coverage_metrics::get_scenario_usage))
381            .route("/api/v2/analytics/personas/ci-hits", get(coverage_metrics::get_persona_ci_hits))
382            .route(
383                "/api/v2/analytics/endpoints/coverage",
384                get(coverage_metrics::get_endpoint_coverage),
385            )
386            .route(
387                "/api/v2/analytics/reality-levels/staleness",
388                get(coverage_metrics::get_reality_level_staleness),
389            )
390            .route(
391                "/api/v2/analytics/drift/percentage",
392                get(coverage_metrics::get_drift_percentage),
393            )
394            .layer(axum::extract::Extension(coverage_state));
395
396        tracing::info!(
397            "Coverage metrics routes mounted at /api/v2/analytics (database initializing)"
398        );
399    }
400
401    // Pillar analytics routes
402    // Note: These routes require an analytics database to be configured.
403    // The handlers will return appropriate errors if the database is not available.
404    // To enable analytics database:
405    // 1. Initialize analytics database connection from config
406    // 2. Add database connection to handler state (e.g., AnalyticsState)
407    // 3. Pass state to analytics route handlers
408    // For now, routes are defined but handlers will return errors if database is not configured
409
410    // Add workspace router with WorkspaceState
411    {
412        use crate::handlers::workspaces::WorkspaceState;
413        use mockforge_core::multi_tenant::{MultiTenantConfig, MultiTenantWorkspaceRegistry};
414        use std::sync::Arc;
415
416        // Create workspace registry
417        let mt_config = MultiTenantConfig {
418            enabled: true,
419            default_workspace: "default".to_string(),
420            ..Default::default()
421        };
422        let registry = MultiTenantWorkspaceRegistry::new(mt_config);
423        let workspace_state = WorkspaceState::new(Arc::new(tokio::sync::RwLock::new(registry)));
424
425        // Create workspace router with state
426        use crate::handlers::workspaces;
427        let workspace_router = Router::new()
428            .route("/__mockforge/workspaces", get(workspaces::list_workspaces))
429            .route("/__mockforge/workspaces", post(workspaces::create_workspace))
430            .route("/__mockforge/workspaces/{workspace_id}", get(workspaces::get_workspace))
431            .route(
432                "/__mockforge/workspaces/{workspace_id}",
433                axum::routing::put(workspaces::update_workspace),
434            )
435            .route("/__mockforge/workspaces/{workspace_id}", delete(workspaces::delete_workspace))
436            .route("/__mockforge/workspaces/{workspace_id}/stats", get(workspaces::get_workspace_stats))
437            // Mock environment endpoints
438            .route("/__mockforge/workspaces/{workspace_id}/environments", get(workspaces::list_mock_environments))
439            .route("/__mockforge/workspaces/{workspace_id}/environments/{env_name}", get(workspaces::get_mock_environment))
440            .route("/__mockforge/workspaces/{workspace_id}/environments/{env_name}", axum::routing::put(workspaces::update_mock_environment))
441            .route("/__mockforge/workspaces/{workspace_id}/environments/active", axum::routing::post(workspaces::set_active_mock_environment))
442            // Note: set_active_workspace handler not yet implemented
443            // .route(
444            //     "/__mockforge/workspaces/{workspace_id}/activate",
445            //     post(workspaces::set_active_workspace),
446            // )
447            .route("/api/v2/voice/create-workspace-confirm", post(voice::create_workspace_confirm))
448            .route(
449                "/__mockforge/voice/create-workspace-confirm",
450                post(voice::create_workspace_confirm),
451            )
452            .with_state(workspace_state);
453
454        router = router.merge(workspace_router);
455        tracing::info!("Workspace router mounted with WorkspaceState");
456
457        // Promotion routes
458        // Initialize PromotionService with database connection if available
459        // Note: In production, this should be initialized at application startup
460        #[cfg(feature = "database-auth")]
461        {
462            use crate::handlers::promotions;
463            use crate::handlers::promotions::PromotionState;
464            use mockforge_collab::promotion::PromotionService;
465            use sqlx::{sqlite::SqlitePoolOptions, Pool, Sqlite};
466            use std::sync::Arc;
467
468            // Try to get database connection from environment or use default
469            let db_url = std::env::var("MOCKFORGE_COLLAB_DB_URL")
470                .unwrap_or_else(|_| "sqlite://mockforge-collab.db".to_string());
471
472            // Note: Database initialization is done lazily when routes are accessed
473            // The promotion handlers will initialize the database connection on first use
474            // For now, promotion routes are commented out until async initialization is properly handled
475            // In production, this should be initialized at application startup before creating the router
476            tracing::debug!("Promotion routes require async database initialization - will be available once database is configured");
477        }
478        #[cfg(not(feature = "database-auth"))]
479        {
480            tracing::debug!("Promotion routes require 'database-auth' feature - not available");
481        }
482    }
483
484    // Add UI Builder router
485    // NOTE: UI Builder initialization is temporarily disabled due to ServerConfig type mismatch
486    // between mockforge-ui and mockforge-http dependency versions. This is a known issue
487    // that will be resolved when dependency versions are aligned.
488    // {
489    //     use mockforge_http::{create_ui_builder_router, UIBuilderState};
490    //     use mockforge_core::config::ServerConfig;
491    //     let server_config = ServerConfig::default();
492    //     let ui_builder_state = UIBuilderState::new(server_config);
493    //     let ui_builder_router = create_ui_builder_router(ui_builder_state);
494    //     router = router.nest_service("/__mockforge/ui-builder", ui_builder_router);
495    //     tracing::info!("UI Builder mounted at /__mockforge/ui-builder");
496    // }
497
498    // SPA fallback: serve index.html for any unmatched routes to support client-side routing
499    // IMPORTANT: This must be AFTER all API routes
500    router = router.route("/{*path}", get(serve_admin_html));
501
502    // Apply RBAC middleware to protected routes
503    // Note: The middleware will check authentication and permissions for all routes
504    // Public routes (auth endpoints, static assets) should be handled gracefully
505    router = router.layer(from_fn(rbac_middleware));
506
507    router
508        .layer(CompressionLayer::new())
509        .layer(CorsLayer::permissive())
510        .with_state(state)
511}
512
513#[cfg(test)]
514mod tests {
515    use super::*;
516
517    #[tokio::test]
518    async fn test_create_admin_router() {
519        let http_addr: std::net::SocketAddr = "127.0.0.1:3000".parse().unwrap();
520        let router = create_admin_router(
521            Some(http_addr),
522            None,
523            None,
524            None,
525            true,
526            8080,
527            "http://localhost:9090".to_string(),
528            None,
529            None,
530            None,
531            None,
532            None,
533        );
534
535        // Router should be created successfully
536        let _ = router;
537    }
538
539    #[tokio::test]
540    async fn test_create_admin_router_no_servers() {
541        let router = create_admin_router(
542            None,
543            None,
544            None,
545            None,
546            false,
547            8080,
548            "http://localhost:9090".to_string(),
549            None,
550            None,
551            None,
552            None,
553            None,
554        );
555
556        // Router should still work without server addresses
557        let _ = router;
558    }
559}