Skip to content
48 changes: 8 additions & 40 deletions crates/hypercolor-daemon/src/api/attachments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,47 +22,15 @@ use crate::api::devices::Pagination;
use crate::api::envelope::ApiResponse;
use crate::domain::{DomainError, ResourceKind};

pub use hypercolor_types::api::attachments::ListTemplatesQuery;

#[derive(Debug, Serialize)]
pub struct TemplateListResponse {
pub items: Vec<TemplateSummary>,
pub pagination: Pagination,
}

#[derive(Debug, Clone, Serialize)]
pub struct TemplateSummary {
pub id: String,
pub name: String,
pub vendor: String,
pub category: ComponentCategory,
pub origin: ComponentOrigin,
pub led_count: u32,
pub description: String,
pub image_url: Option<String>,
pub tags: Vec<String>,
}

#[derive(Debug, Serialize)]
pub struct TemplateDetail {
pub id: String,
pub name: String,
pub vendor: String,
pub category: ComponentCategory,
pub origin: ComponentOrigin,
pub led_count: u32,
pub description: String,
pub default_size: hypercolor_types::attachment::ComponentCanvasSize,
pub topology: hypercolor_types::spatial::LedTopology,
pub led_positions: Vec<hypercolor_types::spatial::NormalizedPosition>,
pub compatible_slots: Vec<hypercolor_types::attachment::ComponentCompatibility>,
pub tags: Vec<String>,
pub led_names: Option<Vec<String>>,
pub led_mapping: Option<Vec<u32>>,
pub image_url: Option<String>,
pub physical_size_mm: Option<(f32, f32)>,
}
// Wire contracts live in hypercolor-types::api::attachments โ€” shared
// with the web UI and the TUI.
pub use hypercolor_types::api::attachments::{
ListTemplatesQuery, TemplateDetail, TemplateListResponse, TemplateSummary,
};

// The category and vendor facets and the per-template item routes are not
// in spec 78's Appendix A, so their shapes stay daemon-local rather than
// entering the shared contract on the way to deletion.
#[derive(Debug, Serialize)]
pub struct CategoryListResponse {
pub items: Vec<CategorySummary>,
Expand Down
69 changes: 35 additions & 34 deletions crates/hypercolor-daemon/src/api/effects.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ use axum::http::{HeaderMap, HeaderValue, header};
use axum::response::{IntoResponse, Response};
use base64::Engine as _;
use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
use serde::Serialize;
use tokio::fs;
use tracing::{info, warn};

Expand Down Expand Up @@ -64,10 +63,11 @@ pub(crate) async fn invalidate_active_render_groups_after_effect_registry_update
// web UI and the TUI.
pub use hypercolor_types::api::effects::{
ActiveEffectResponse, ApplyEffectPresetRequest, ApplyEffectRequest, ApplyEffectResponse,
ApplyTransitionResponse, EffectCapabilitySet, EffectDetailResponse, EffectLayoutApplyResult,
EffectListResponse, EffectPresetListResponse, EffectPresetOrigin, EffectPresetSummary,
EffectRefSummary, EffectSummary, InstalledEffectResponse, LayoutLinkSummary,
PauseEffectResponse, ResetControlsRequest, ResumeEffectResponse, TransitionRequest,
ApplyTransitionResponse, DeleteEffectLayoutResponse, EffectCapabilitySet, EffectDetailResponse,
EffectLayoutApplyResult, EffectLayoutResponse, EffectListResponse, EffectPresetListResponse,
EffectPresetOrigin, EffectPresetSummary, EffectRefSummary, EffectSummary,
InstalledEffectResponse, LayoutLinkSummary, PauseEffectResponse, RescanResponse,
ResetControlsRequest, ResumeEffectResponse, SetEffectLayoutResponse, TransitionRequest,
UpdateActiveControlsRequest,
};

Expand Down Expand Up @@ -499,15 +499,15 @@ pub async fn get_effect_layout(
};

let summary = layout.as_ref().map(layout_link_summary);
ApiResponse::ok(serde_json::json!({
"effect": {
"id": effect_id,
"name": effect.name,
ApiResponse::ok(EffectLayoutResponse {
effect: EffectRefSummary {
id: effect_id,
name: effect.name,
},
"layout_id": layout_id,
"resolved": summary.is_some(),
"layout": summary,
}))
layout_id,
resolved: summary.is_some(),
layout: summary,
})
}

/// `PUT /api/v1/effects/:id/layout` โ€” Associate an effect with a layout.
Expand Down Expand Up @@ -572,14 +572,14 @@ pub async fn set_effect_layout(
return DomainError::Internal(anyhow::anyhow!(error)).into_response();
}

ApiResponse::ok(serde_json::json!({
"effect": {
"id": effect_id,
"name": effect.name,
ApiResponse::ok(SetEffectLayoutResponse {
effect: EffectRefSummary {
id: effect_id,
name: effect.name,
},
"layout": layout_link_summary(&layout),
"linked": true,
}))
layout: layout_link_summary(&layout),
linked: true,
})
}

/// `DELETE /api/v1/effects/:id/layout` โ€” Remove an effect -> layout association.
Expand Down Expand Up @@ -623,14 +623,14 @@ pub async fn delete_effect_layout(
return DomainError::Internal(anyhow::anyhow!(error)).into_response();
}

ApiResponse::ok(serde_json::json!({
"effect": {
"id": effect_id,
"name": effect.name,
ApiResponse::ok(DeleteEffectLayoutResponse {
effect: EffectRefSummary {
id: effect_id,
name: effect.name,
},
"layout_id": removed_layout_id,
"deleted": removed_layout_id.is_some(),
}))
deleted: removed_layout_id.is_some(),
layout_id: removed_layout_id,
})
}

/// `POST /api/v1/effects/:id/apply` โ€” Start rendering an effect.
Expand Down Expand Up @@ -1001,6 +1001,10 @@ pub async fn update_active_controls(
);
}

// Held back from the wave 3.1c type promotion deliberately: `applied`
// carries f32 control values, and `json!` widens them to f64 while a
// derived struct does not, so naming this shape would reprint every
// non-representable float (0.1 -> 0.10000000149011612 today).
ApiResponse::ok(serde_json::json!({
"effect": effect_name,
"applied": applied,
Expand Down Expand Up @@ -1109,6 +1113,7 @@ pub async fn update_effect_controls(
);
}

// Held back for the same f32 reprint reason as its `active` sibling.
let body = ApiResponse::ok(serde_json::json!({
"effect": effect_name,
"applied": applied,
Expand Down Expand Up @@ -1218,6 +1223,9 @@ pub async fn set_active_control_binding(
Err(error) => return error.into_response(),
}

// Held back: `ControlBinding` is six f32 fields, which a named struct
// would reprint at f32 precision instead of the widened f64 form the
// literal has always emitted.
ApiResponse::ok(serde_json::json!({
"effect": {
"id": effect_id,
Expand Down Expand Up @@ -1427,13 +1435,6 @@ pub async fn install_effect(
})
}

#[derive(Debug, Serialize)]
pub struct RescanResponse {
pub added: usize,
pub removed: usize,
pub updated: usize,
}

pub(crate) fn resolve_effect_metadata(
registry: &EffectRegistry,
id_or_name: &str,
Expand Down
39 changes: 14 additions & 25 deletions crates/hypercolor-daemon/src/api/library/favorites.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ use axum::Json;
use axum::extract::{Path, State};
use axum::response::{IntoResponse, Response};
use hypercolor_types::event::{HypercolorEvent, LibraryChangeKind, LibraryCollection};
use serde::Serialize;

use crate::api::AppState;
use crate::api::effects::resolve_effect_metadata;
Expand All @@ -16,22 +15,12 @@ use crate::domain::{DomainError, ResourceKind};

use super::unix_epoch_ms;

pub use hypercolor_types::api::library::AddFavoriteRequest;

// โ”€โ”€ Request / Response Types โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

#[derive(Debug, Serialize)]
pub struct FavoriteSummary {
pub effect_id: String,
pub effect_name: String,
pub added_at_ms: u64,
}

#[derive(Debug, Serialize)]
pub struct FavoriteListResponse {
pub items: Vec<FavoriteSummary>,
pub pagination: crate::api::devices::Pagination,
}
// Wire contracts live in hypercolor-types::api::library โ€” shared with
// the web UI and the TUI.
pub use hypercolor_types::api::library::{
AddFavoriteRequest, AddFavoriteResponse, DeleteFavoriteResponse, FavoriteListResponse,
FavoriteSummary,
};

// โ”€โ”€ Handlers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

Expand Down Expand Up @@ -105,14 +94,14 @@ pub async fn add_favorite(
kind: LibraryChangeKind::Upserted,
});

ApiResponse::ok(serde_json::json!({
"favorite": FavoriteSummary {
ApiResponse::ok(AddFavoriteResponse {
favorite: FavoriteSummary {
effect_id: favorite.effect_id.to_string(),
effect_name: effect.name,
added_at_ms: favorite.added_at_ms,
},
"created": !existing,
}))
created: !existing,
})
}

/// `DELETE /api/v1/library/favorites/:effect` โ€” remove a favorite by effect id/name.
Expand Down Expand Up @@ -143,8 +132,8 @@ pub async fn remove_favorite(
kind: LibraryChangeKind::Removed,
});

ApiResponse::ok(serde_json::json!({
"effect_id": effect.id.to_string(),
"deleted": true,
}))
ApiResponse::ok(DeleteFavoriteResponse {
effect_id: effect.id.to_string(),
deleted: true,
})
}
56 changes: 21 additions & 35 deletions crates/hypercolor-daemon/src/api/library/playlists.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ use std::time::Duration;
use axum::Json;
use axum::extract::{Path, State};
use axum::response::{IntoResponse, Response};
use serde::Serialize;
use tokio::sync::watch;
use tracing::warn;

Expand All @@ -27,29 +26,16 @@ use super::{
store_error_to_response, unix_epoch_ms,
};

// Wire contracts live in hypercolor-types::api::library โ€” shared with
// the web UI and the TUI.
pub use hypercolor_types::api::library::{
PlaylistItemRequest, PlaylistTargetRequest, SavePlaylistRequest,
ActivatePlaylistResponse, ActivePlaylistResponse, ActivePlaylistStateResponse,
DeletePlaylistResponse, PlaylistItemRequest, PlaylistListResponse, PlaylistTargetRequest,
SavePlaylistRequest, StopPlaylistResponse,
};

const DEFAULT_PLAYLIST_ITEM_DURATION_MS: u64 = 30_000;

// โ”€โ”€ Request / Response Types โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

#[derive(Debug, Serialize)]
pub struct PlaylistListResponse {
pub items: Vec<EffectPlaylist>,
pub pagination: crate::api::devices::Pagination,
}

#[derive(Debug, Clone, Serialize)]
pub struct ActivePlaylistResponse {
pub id: String,
pub name: String,
pub loop_enabled: bool,
pub item_count: usize,
pub started_at_ms: u64,
}

// โ”€โ”€ Handlers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

/// `GET /api/v1/library/playlists` โ€” list all playlists.
Expand Down Expand Up @@ -216,10 +202,10 @@ pub async fn delete_playlist(
};
stop_runtime(active);

ApiResponse::ok(serde_json::json!({
"id": playlist_id.to_string(),
"deleted": true,
}))
ApiResponse::ok(DeletePlaylistResponse {
id: playlist_id.to_string(),
deleted: true,
})
}

/// `POST /api/v1/library/playlists/:id/activate` โ€” start playlist playback.
Expand Down Expand Up @@ -284,10 +270,10 @@ pub async fn activate_playlist(
runtime.active = Some(active);
}

ApiResponse::ok(serde_json::json!({
"playlist": response_payload,
"active": true,
}))
ApiResponse::ok(ActivatePlaylistResponse {
playlist: response_payload,
active: true,
})
}

/// `GET /api/v1/library/playlists/active` โ€” inspect the active playlist runtime.
Expand All @@ -297,10 +283,10 @@ pub async fn get_active_playlist(State(state): State<Arc<AppState>>) -> Response
return DomainError::not_found(ResourceKind::Playlist, "active").into_response();
};

ApiResponse::ok(serde_json::json!({
"playlist": active_playlist_payload(active),
"state": "running",
}))
ApiResponse::ok(ActivePlaylistStateResponse {
playlist: active_playlist_payload(active),
state: "running".to_owned(),
})
}

/// `POST /api/v1/library/playlists/stop` โ€” stop playlist playback if active.
Expand All @@ -316,10 +302,10 @@ pub async fn stop_playlist(State(state): State<Arc<AppState>>) -> Response {
let payload = active_playlist_payload(&active);
stop_runtime(Some(active));

ApiResponse::ok(serde_json::json!({
"playlist": payload,
"stopped": true,
}))
ApiResponse::ok(StopPlaylistResponse {
playlist: payload,
stopped: true,
})
}

// โ”€โ”€ Helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
Expand Down
Loading