const DASHBOARD_PRESET_MAX: usize = 20; const DASHBOARD_PRESET_NAME_MAX: usize = 64; #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(deny_unknown_fields)] struct DashboardPreset { name: String, dashboard_density: String, dashboard_widgets: Vec, dashboard_hidden_widgets: Vec, #[serde(default)] dashboard_widget_sizes: Option>, } fn reconcile_dashboard_layout( order: &mut Vec, hidden: &mut Vec, sizes: &mut Option>, ) { let mut seen = std::collections::HashSet::new(); order.retain(|id| { DASHBOARD_WIDGETS.iter().any(|widget| widget.id == *id) && seen.insert(id.clone()) }); let mut seen_hidden = std::collections::HashSet::new(); hidden.retain(|id| { DASHBOARD_WIDGETS.iter().any(|widget| widget.id == *id) && seen_hidden.insert(id.clone()) }); if let Some(sizes) = sizes.as_mut() { sizes.retain(|id, span| { DASHBOARD_WIDGETS .iter() .any(|widget| widget.id == *id && valid_dashboard_span(&widget.display_type, *span)) }); } for widget in DASHBOARD_WIDGETS.iter() { if !seen.contains(&widget.id) { order.push(widget.id.clone()); if widget.default_hidden && seen_hidden.insert(widget.id.clone()) { hidden.push(widget.id.clone()); } } } } fn reconcile_dashboard_presets(presets: &mut [DashboardPreset]) { for preset in presets { reconcile_dashboard_layout( &mut preset.dashboard_widgets, &mut preset.dashboard_hidden_widgets, &mut preset.dashboard_widget_sizes, ); } } fn valid_dashboard_preset_name(name: &str) -> bool { !name.is_empty() && name.chars().count() <= DASHBOARD_PRESET_NAME_MAX && name.trim() == name && !name.chars().any(char::is_control) } fn valid_dashboard_preset(preset: &DashboardPreset) -> bool { if !valid_dashboard_preset_name(&preset.name) || !matches!(preset.dashboard_density.as_str(), "normal" | "compact" | "ultra") || preset.dashboard_widgets.is_empty() || preset.dashboard_widgets.len() > DASHBOARD_WIDGETS.len() || preset.dashboard_hidden_widgets.len() > DASHBOARD_WIDGETS.len() || !valid_dashboard_sizes(&preset.dashboard_widget_sizes) { return false; } let mut order = std::collections::HashSet::new(); if preset.dashboard_widgets.iter().any(|id| { !DASHBOARD_WIDGETS.iter().any(|widget| widget.id == *id) || !order.insert(id.as_str()) }) { return false; } let mut hidden = std::collections::HashSet::new(); !preset.dashboard_hidden_widgets.iter().any(|id| { !order.contains(id.as_str()) || !hidden.insert(id.as_str()) }) } fn validate_dashboard_presets(presets: &[DashboardPreset]) -> Result<(), ApiError> { if presets.len() > DASHBOARD_PRESET_MAX { return Err(ApiError( StatusCode::UNPROCESSABLE_ENTITY, "too many dashboard presets".into(), )); } let mut names = std::collections::HashSet::new(); for preset in presets { if !valid_dashboard_preset(preset) || !names.insert(preset.name.to_lowercase()) { return Err(ApiError( StatusCode::UNPROCESSABLE_ENTITY, "invalid dashboard preset".into(), )); } } Ok(()) }