Tighten web surfaces and clean handoff
This commit is contained in:
@@ -22,7 +22,7 @@ impl Default for TickSchedule {
|
|||||||
logic_hz: 120,
|
logic_hz: 120,
|
||||||
frame_synthesis_hz: 60,
|
frame_synthesis_hz: 60,
|
||||||
network_send_hz: 60,
|
network_send_hz: 60,
|
||||||
preview_hz: 15,
|
preview_hz: 60,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -54,12 +54,12 @@ impl RealtimeEngine {
|
|||||||
mode: ValidationMode,
|
mode: ValidationMode,
|
||||||
) -> ValidationReport {
|
) -> ValidationReport {
|
||||||
let mut report = project.validate(mode);
|
let mut report = project.validate(mode);
|
||||||
if self.schedule.preview_hz >= self.schedule.frame_synthesis_hz {
|
if self.schedule.preview_hz > self.schedule.frame_synthesis_hz {
|
||||||
report.issues.push(ValidationIssue {
|
report.issues.push(ValidationIssue {
|
||||||
severity: ValidationSeverity::Warning,
|
severity: ValidationSeverity::Warning,
|
||||||
code: "preview_rate_too_high",
|
code: "preview_rate_too_high",
|
||||||
path: "runtime.schedule.preview_hz".to_string(),
|
path: "runtime.schedule.preview_hz".to_string(),
|
||||||
message: "preview rate should stay below frame synthesis rate".to_string(),
|
message: "preview rate should not exceed frame synthesis rate".to_string(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
report
|
report
|
||||||
|
|||||||
@@ -317,7 +317,7 @@ fn render_pattern_leds(
|
|||||||
let speed = scene_scalar(scene, "speed", 0.45).max(0.05);
|
let speed = scene_scalar(scene, "speed", 0.45).max(0.05);
|
||||||
let tempo_multiplier = scene_scalar(scene, "tempo_multiplier", 1.0).clamp(0.25, 8.0);
|
let tempo_multiplier = scene_scalar(scene, "tempo_multiplier", 1.0).clamp(0.25, 8.0);
|
||||||
let intensity = scene_scalar(scene, "intensity", 1.0).clamp(0.0, 1.0);
|
let intensity = scene_scalar(scene, "intensity", 1.0).clamp(0.0, 1.0);
|
||||||
let fade = scene_scalar(scene, "fade", 0.35).clamp(0.0, 1.0);
|
let fade = scene_scalar(scene, "fade", 0.0).clamp(0.0, 1.0);
|
||||||
let pattern_brightness = scene_scalar(scene, "brightness", 1.0).clamp(0.0, 2.0);
|
let pattern_brightness = scene_scalar(scene, "brightness", 1.0).clamp(0.0, 2.0);
|
||||||
let block_size = scene_scalar(scene, "block_size", 1.0).max(0.1);
|
let block_size = scene_scalar(scene, "block_size", 1.0).max(0.1);
|
||||||
let on_width = scene_scalar(scene, "on_width", 1.0).clamp(0.1, 2.0);
|
let on_width = scene_scalar(scene, "on_width", 1.0).clamp(0.1, 2.0);
|
||||||
@@ -543,16 +543,31 @@ fn render_pattern_leds(
|
|||||||
_ => vec![primary; led_count],
|
_ => vec![primary; led_count],
|
||||||
};
|
};
|
||||||
|
|
||||||
if fade > 0.0 && colors.len() > 2 {
|
if fade > 0.0 && colors.len() > 2 && smoothing_allowed_for_pattern(pattern_id) {
|
||||||
colors = smooth_led_sequence(colors, fade);
|
colors = smooth_led_sequence(colors, fade);
|
||||||
}
|
}
|
||||||
|
|
||||||
colors
|
colors
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|color| color.scale((intensity * pattern_brightness).clamp(0.0, 1.0)))
|
.map(|color| {
|
||||||
|
color
|
||||||
|
.scale(quantize_intensity(
|
||||||
|
(intensity * pattern_brightness).clamp(0.0, 1.0),
|
||||||
|
))
|
||||||
|
.quantize_10_percent()
|
||||||
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn smoothing_allowed_for_pattern(pattern_id: &str) -> bool {
|
||||||
|
matches!(pattern_id, "breathing")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn quantize_intensity(value: f32) -> f32 {
|
||||||
|
let clamped = value.clamp(0.0, 1.0);
|
||||||
|
(clamped * 10.0).round() / 10.0
|
||||||
|
}
|
||||||
|
|
||||||
fn canonical_pattern_id(pattern_id: &str) -> &str {
|
fn canonical_pattern_id(pattern_id: &str) -> &str {
|
||||||
match pattern_id {
|
match pattern_id {
|
||||||
"solid_color" => "solid",
|
"solid_color" => "solid",
|
||||||
@@ -715,7 +730,7 @@ fn choose_pair(
|
|||||||
) -> (RgbColor, RgbColor) {
|
) -> (RgbColor, RgbColor) {
|
||||||
let primary = scene_text(scene, "primary_color", "#4D7CFF");
|
let primary = scene_text(scene, "primary_color", "#4D7CFF");
|
||||||
let secondary = scene_text(scene, "secondary_color", "#0E1630");
|
let secondary = scene_text(scene, "secondary_color", "#0E1630");
|
||||||
let palette_name = scene_text(scene, "palette", "Laser Club");
|
let palette_name = scene_text(scene, "palette", "Deep Blue");
|
||||||
let seed_amount = amount + panel_row as f32 * 0.13 + panel_col as f32 * 0.07;
|
let seed_amount = amount + panel_row as f32 * 0.13 + panel_col as f32 * 0.07;
|
||||||
match color_mode {
|
match color_mode {
|
||||||
"palette" => (
|
"palette" => (
|
||||||
@@ -770,159 +785,129 @@ fn sample_palette_list(colors: &[RgbColor], amount: f32) -> RgbColor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn named_palette(name: &str) -> Option<&'static [RgbColor]> {
|
fn named_palette(name: &str) -> Option<&'static [RgbColor]> {
|
||||||
const LASER_CLUB: [RgbColor; 4] = [
|
const CANDLE: [RgbColor; 1] = [RgbColor {
|
||||||
RgbColor {
|
|
||||||
r: 0,
|
|
||||||
g: 240,
|
|
||||||
b: 255,
|
|
||||||
},
|
|
||||||
RgbColor {
|
|
||||||
r: 0,
|
|
||||||
g: 140,
|
|
||||||
b: 255,
|
|
||||||
},
|
|
||||||
RgbColor {
|
|
||||||
r: 106,
|
|
||||||
g: 0,
|
|
||||||
b: 255,
|
|
||||||
},
|
|
||||||
RgbColor { r: 6, g: 8, b: 20 },
|
|
||||||
];
|
|
||||||
const AFTERHOURS: [RgbColor; 4] = [
|
|
||||||
RgbColor {
|
|
||||||
r: 247,
|
|
||||||
g: 37,
|
|
||||||
b: 133,
|
|
||||||
},
|
|
||||||
RgbColor {
|
|
||||||
r: 181,
|
|
||||||
g: 23,
|
|
||||||
b: 158,
|
|
||||||
},
|
|
||||||
RgbColor {
|
|
||||||
r: 114,
|
|
||||||
g: 9,
|
|
||||||
b: 183,
|
|
||||||
},
|
|
||||||
RgbColor { r: 20, g: 3, b: 26 },
|
|
||||||
];
|
|
||||||
const VOLTAGE: [RgbColor; 4] = [
|
|
||||||
RgbColor {
|
|
||||||
r: 0,
|
|
||||||
g: 229,
|
|
||||||
b: 255,
|
|
||||||
},
|
|
||||||
RgbColor {
|
|
||||||
r: 0,
|
|
||||||
g: 179,
|
|
||||||
b: 255,
|
|
||||||
},
|
|
||||||
RgbColor {
|
|
||||||
r: 58,
|
|
||||||
g: 134,
|
|
||||||
b: 255,
|
|
||||||
},
|
|
||||||
RgbColor { r: 5, g: 10, b: 20 },
|
|
||||||
];
|
|
||||||
const MAGENTA_DRIVE: [RgbColor; 4] = [
|
|
||||||
RgbColor {
|
|
||||||
r: 255,
|
r: 255,
|
||||||
g: 0,
|
g: 147,
|
||||||
b: 110,
|
b: 41,
|
||||||
},
|
}];
|
||||||
RgbColor {
|
const TUNGSTEN_40W: [RgbColor; 1] = [RgbColor {
|
||||||
r: 255,
|
r: 255,
|
||||||
g: 77,
|
g: 197,
|
||||||
b: 166,
|
b: 143,
|
||||||
},
|
}];
|
||||||
RgbColor {
|
const TUNGSTEN_100W: [RgbColor; 1] = [RgbColor {
|
||||||
r: 122,
|
r: 255,
|
||||||
g: 0,
|
g: 214,
|
||||||
|
b: 170,
|
||||||
|
}];
|
||||||
|
const HALOGEN: [RgbColor; 1] = [RgbColor {
|
||||||
|
r: 255,
|
||||||
|
g: 241,
|
||||||
|
b: 224,
|
||||||
|
}];
|
||||||
|
const CARBON_ARC: [RgbColor; 1] = [RgbColor {
|
||||||
|
r: 255,
|
||||||
|
g: 250,
|
||||||
|
b: 244,
|
||||||
|
}];
|
||||||
|
const HIGH_NOON_SUN: [RgbColor; 1] = [RgbColor {
|
||||||
|
r: 255,
|
||||||
|
g: 255,
|
||||||
|
b: 251,
|
||||||
|
}];
|
||||||
|
const OVERCAST_SKY: [RgbColor; 1] = [RgbColor {
|
||||||
|
r: 201,
|
||||||
|
g: 226,
|
||||||
b: 255,
|
b: 255,
|
||||||
},
|
}];
|
||||||
RgbColor { r: 18, g: 3, b: 24 },
|
const CLEAR_BLUE_SKY: [RgbColor; 1] = [RgbColor {
|
||||||
];
|
r: 64,
|
||||||
const WAREHOUSE_HEAT: [RgbColor; 4] = [
|
g: 156,
|
||||||
RgbColor {
|
b: 255,
|
||||||
r: 255,
|
}];
|
||||||
g: 90,
|
|
||||||
b: 31,
|
const DEEP_BLUE: [RgbColor; 1] = [RgbColor {
|
||||||
},
|
r: 0,
|
||||||
RgbColor {
|
g: 71,
|
||||||
r: 255,
|
b: 255,
|
||||||
g: 158,
|
}];
|
||||||
|
const ELECTRIC_CYAN: [RgbColor; 1] = [RgbColor {
|
||||||
|
r: 0,
|
||||||
|
g: 216,
|
||||||
|
b: 255,
|
||||||
|
}];
|
||||||
|
const EMERALD: [RgbColor; 1] = [RgbColor {
|
||||||
|
r: 0,
|
||||||
|
g: 200,
|
||||||
|
b: 83,
|
||||||
|
}];
|
||||||
|
const ACID_LIME: [RgbColor; 1] = [RgbColor {
|
||||||
|
r: 174,
|
||||||
|
g: 234,
|
||||||
b: 0,
|
b: 0,
|
||||||
},
|
}];
|
||||||
RgbColor {
|
const AMBER: [RgbColor; 1] = [RgbColor {
|
||||||
r: 255,
|
r: 255,
|
||||||
g: 208,
|
g: 157,
|
||||||
b: 0,
|
b: 0,
|
||||||
},
|
}];
|
||||||
RgbColor { r: 20, g: 6, b: 0 },
|
const SIGNAL_RED: [RgbColor; 1] = [RgbColor {
|
||||||
];
|
r: 255,
|
||||||
const UV_RIOT: [RgbColor; 4] = [
|
g: 45,
|
||||||
RgbColor {
|
b: 45,
|
||||||
r: 122,
|
}];
|
||||||
g: 0,
|
const HOT_MAGENTA: [RgbColor; 1] = [RgbColor {
|
||||||
b: 255,
|
|
||||||
},
|
|
||||||
RgbColor {
|
|
||||||
r: 177,
|
|
||||||
g: 0,
|
|
||||||
b: 255,
|
|
||||||
},
|
|
||||||
RgbColor {
|
|
||||||
r: 255,
|
r: 255,
|
||||||
g: 0,
|
g: 0,
|
||||||
b: 168,
|
b: 168,
|
||||||
},
|
}];
|
||||||
RgbColor { r: 16, g: 0, b: 20 },
|
const VIOLET: [RgbColor; 1] = [RgbColor {
|
||||||
];
|
r: 122,
|
||||||
const REDLINE: [RgbColor; 4] = [
|
g: 60,
|
||||||
RgbColor {
|
b: 255,
|
||||||
|
}];
|
||||||
|
const WARM_WHITE: [RgbColor; 1] = [RgbColor {
|
||||||
r: 255,
|
r: 255,
|
||||||
g: 45,
|
g: 214,
|
||||||
b: 85,
|
b: 170,
|
||||||
},
|
}];
|
||||||
RgbColor {
|
const NEUTRAL_WHITE: [RgbColor; 1] = [RgbColor {
|
||||||
r: 255,
|
r: 255,
|
||||||
g: 106,
|
g: 241,
|
||||||
b: 0,
|
b: 224,
|
||||||
},
|
}];
|
||||||
RgbColor {
|
const COOL_WHITE: [RgbColor; 1] = [RgbColor {
|
||||||
r: 255,
|
r: 201,
|
||||||
g: 176,
|
g: 226,
|
||||||
b: 0,
|
b: 255,
|
||||||
},
|
}];
|
||||||
RgbColor { r: 22, g: 4, b: 6 },
|
const BLACKLIGHT_VIOLET: [RgbColor; 1] = [RgbColor {
|
||||||
];
|
r: 167,
|
||||||
const SODIUM_HAZE: [RgbColor; 4] = [
|
g: 0,
|
||||||
RgbColor {
|
b: 255,
|
||||||
r: 255,
|
}];
|
||||||
g: 122,
|
|
||||||
b: 0,
|
|
||||||
},
|
|
||||||
RgbColor {
|
|
||||||
r: 255,
|
|
||||||
g: 176,
|
|
||||||
b: 0,
|
|
||||||
},
|
|
||||||
RgbColor {
|
|
||||||
r: 255,
|
|
||||||
g: 216,
|
|
||||||
b: 107,
|
|
||||||
},
|
|
||||||
RgbColor { r: 18, g: 7, b: 0 },
|
|
||||||
];
|
|
||||||
match name {
|
match name {
|
||||||
"Magenta Drive" => Some(&MAGENTA_DRIVE),
|
"Candle" => Some(&CANDLE),
|
||||||
"Warehouse Heat" => Some(&WAREHOUSE_HEAT),
|
"Tungsten 40W" => Some(&TUNGSTEN_40W),
|
||||||
"UV Riot" => Some(&UV_RIOT),
|
"Tungsten 100W" => Some(&TUNGSTEN_100W),
|
||||||
"Redline" => Some(&REDLINE),
|
"Halogen" => Some(&HALOGEN),
|
||||||
"Sodium Haze" => Some(&SODIUM_HAZE),
|
"Carbon Arc" => Some(&CARBON_ARC),
|
||||||
"Afterhours" => Some(&AFTERHOURS),
|
"High Noon Sun" => Some(&HIGH_NOON_SUN),
|
||||||
"Voltage" => Some(&VOLTAGE),
|
"Overcast Sky" => Some(&OVERCAST_SKY),
|
||||||
"Laser Club" => Some(&LASER_CLUB),
|
"Clear Blue Sky" => Some(&CLEAR_BLUE_SKY),
|
||||||
|
"Deep Blue" => Some(&DEEP_BLUE),
|
||||||
|
"Electric Cyan" => Some(&ELECTRIC_CYAN),
|
||||||
|
"Emerald" => Some(&EMERALD),
|
||||||
|
"Acid Lime" => Some(&ACID_LIME),
|
||||||
|
"Amber" => Some(&AMBER),
|
||||||
|
"Signal Red" => Some(&SIGNAL_RED),
|
||||||
|
"Hot Magenta" => Some(&HOT_MAGENTA),
|
||||||
|
"Violet" => Some(&VIOLET),
|
||||||
|
"Warm White" => Some(&WARM_WHITE),
|
||||||
|
"Neutral White" => Some(&NEUTRAL_WHITE),
|
||||||
|
"Cool White" => Some(&COOL_WHITE),
|
||||||
|
"Blacklight Violet" => Some(&BLACKLIGHT_VIOLET),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1853,12 +1838,12 @@ fn common_motion_parameters(extra_keys: Vec<&str>) -> Vec<SceneParameterSpec> {
|
|||||||
scalar_spec("speed", "Speed", 0.05, 8.0, 0.05, 0.45),
|
scalar_spec("speed", "Speed", 0.05, 8.0, 0.05, 0.45),
|
||||||
scalar_spec("intensity", "Intensity", 0.0, 1.0, 0.01, 1.0),
|
scalar_spec("intensity", "Intensity", 0.0, 1.0, 0.01, 1.0),
|
||||||
scalar_spec("brightness", "Brightness", 0.0, 2.0, 0.01, 1.0),
|
scalar_spec("brightness", "Brightness", 0.0, 2.0, 0.01, 1.0),
|
||||||
scalar_spec("fade", "Smoothing", 0.0, 1.0, 0.01, 0.35),
|
scalar_spec("fade", "Smoothing", 0.0, 1.0, 0.01, 0.0),
|
||||||
scalar_spec("tempo_multiplier", "Tempo Multiplier", 0.25, 8.0, 0.05, 1.0),
|
scalar_spec("tempo_multiplier", "Tempo Multiplier", 0.25, 8.0, 0.05, 1.0),
|
||||||
text_spec("color_mode", "Color Mode", "dual"),
|
text_spec("color_mode", "Color Mode", "dual"),
|
||||||
text_spec("primary_color", "Primary Color", "#4D7CFF"),
|
text_spec("primary_color", "Primary Color", "#4D7CFF"),
|
||||||
text_spec("secondary_color", "Secondary Color", "#0E1630"),
|
text_spec("secondary_color", "Secondary Color", "#0E1630"),
|
||||||
text_spec("palette", "Palette", "Laser Club"),
|
text_spec("palette", "Palette", "Deep Blue"),
|
||||||
];
|
];
|
||||||
|
|
||||||
for key in extra_keys {
|
for key in extra_keys {
|
||||||
@@ -1960,6 +1945,14 @@ impl RgbColor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn quantize_10_percent(self) -> Self {
|
||||||
|
Self {
|
||||||
|
r: quantize_channel_10_percent(self.r),
|
||||||
|
g: quantize_channel_10_percent(self.g),
|
||||||
|
b: quantize_channel_10_percent(self.b),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn complementary(self) -> Self {
|
fn complementary(self) -> Self {
|
||||||
Self {
|
Self {
|
||||||
r: 255u8.saturating_sub(self.r),
|
r: 255u8.saturating_sub(self.r),
|
||||||
@@ -1998,6 +1991,12 @@ impl RgbColor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn quantize_channel_10_percent(value: u8) -> u8 {
|
||||||
|
(((value as f32 / 255.0) * 10.0).round() * 25.5)
|
||||||
|
.round()
|
||||||
|
.clamp(0.0, 255.0) as u8
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
@@ -277,6 +277,7 @@ fn handle_websocket(
|
|||||||
let mut sequence = 1u64;
|
let mut sequence = 1u64;
|
||||||
let mut last_event_millis = None::<u64>;
|
let mut last_event_millis = None::<u64>;
|
||||||
let mut last_event_signatures = Vec::<(Option<String>, String)>::new();
|
let mut last_event_signatures = Vec::<(Option<String>, String)>::new();
|
||||||
|
let mut last_streamed_preview = None::<crate::dto::ApiPreviewSnapshot>;
|
||||||
loop {
|
loop {
|
||||||
let snapshot = service.snapshot();
|
let snapshot = service.snapshot();
|
||||||
send_stream_message(
|
send_stream_message(
|
||||||
@@ -286,13 +287,22 @@ fn handle_websocket(
|
|||||||
ApiStreamMessage::Snapshot(ApiStateSnapshot::from_snapshot(&snapshot)),
|
ApiStreamMessage::Snapshot(ApiStateSnapshot::from_snapshot(&snapshot)),
|
||||||
)?;
|
)?;
|
||||||
sequence += 1;
|
sequence += 1;
|
||||||
|
|
||||||
|
let preview_payload = crate::dto::ApiPreviewSnapshot::from_snapshot(&snapshot);
|
||||||
|
if last_streamed_preview
|
||||||
|
.as_ref()
|
||||||
|
.map(|previous| previous != &preview_payload)
|
||||||
|
.unwrap_or(true)
|
||||||
|
{
|
||||||
send_stream_message(
|
send_stream_message(
|
||||||
&mut stream,
|
&mut stream,
|
||||||
sequence,
|
sequence,
|
||||||
snapshot.generated_at_millis,
|
snapshot.generated_at_millis,
|
||||||
ApiStreamMessage::Preview(crate::dto::ApiPreviewSnapshot::from_snapshot(&snapshot)),
|
ApiStreamMessage::Preview(preview_payload.clone()),
|
||||||
)?;
|
)?;
|
||||||
sequence += 1;
|
sequence += 1;
|
||||||
|
last_streamed_preview = Some(preview_payload);
|
||||||
|
}
|
||||||
|
|
||||||
let mut new_events = snapshot
|
let mut new_events = snapshot
|
||||||
.recent_events
|
.recent_events
|
||||||
|
|||||||
@@ -147,6 +147,28 @@ fn web_ui_browser_smoke_serves_shell_assets_and_stream_bootstrap() {
|
|||||||
server.shutdown();
|
server.shutdown();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn technical_surface_script_guards_missing_recent_events_in_state_snapshot() {
|
||||||
|
let server = start_server();
|
||||||
|
let technical_js = send_http_request(server.local_addr(), "GET", "/technical.js", None);
|
||||||
|
let state = send_http_request(server.local_addr(), "GET", "/api/v1/state", None);
|
||||||
|
let state_body: Value = serde_json::from_str(&state.body).expect("state json");
|
||||||
|
|
||||||
|
assert_eq!(technical_js.status_code, 200);
|
||||||
|
assert!(technical_js
|
||||||
|
.body
|
||||||
|
.contains("function snapshotRecentEvents(snapshot)"));
|
||||||
|
assert!(technical_js
|
||||||
|
.body
|
||||||
|
.contains("Array.isArray(snapshot?.recent_events) ? snapshot.recent_events : []"));
|
||||||
|
assert!(technical_js
|
||||||
|
.body
|
||||||
|
.contains("const recentEvents = snapshotRecentEvents(appState.snapshot);"));
|
||||||
|
assert!(state_body["state"].get("recent_events").is_none());
|
||||||
|
|
||||||
|
server.shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn state_preview_and_snapshot_endpoints_are_versioned_and_separated() {
|
fn state_preview_and_snapshot_endpoints_are_versioned_and_separated() {
|
||||||
let server = start_server();
|
let server = start_server();
|
||||||
@@ -268,6 +290,40 @@ fn technical_surface_commands_update_backend_node_targets_and_panel_mapping() {
|
|||||||
server.shutdown();
|
server.shutdown();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn technical_surface_can_disable_output_again_after_enabling_it() {
|
||||||
|
let server = start_server();
|
||||||
|
|
||||||
|
let enable_mode = send_command_json(
|
||||||
|
server.local_addr(),
|
||||||
|
r#"{"command":{"type":"set_output_backend_mode","payload":{"mode":"ddp_wled"}}}"#,
|
||||||
|
);
|
||||||
|
let enable_output = send_command_json(
|
||||||
|
server.local_addr(),
|
||||||
|
r#"{"command":{"type":"set_live_output_enabled","payload":{"enabled":true}}}"#,
|
||||||
|
);
|
||||||
|
let disable_output = send_command_json(
|
||||||
|
server.local_addr(),
|
||||||
|
r#"{"command":{"type":"set_live_output_enabled","payload":{"enabled":false}}}"#,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(enable_mode.status_code, 200);
|
||||||
|
assert_eq!(enable_output.status_code, 200);
|
||||||
|
assert_eq!(disable_output.status_code, 200);
|
||||||
|
|
||||||
|
let state = send_http_request(server.local_addr(), "GET", "/api/v1/state", None);
|
||||||
|
let state_body: Value = serde_json::from_str(&state.body).expect("state json");
|
||||||
|
|
||||||
|
assert_eq!(state_body["state"]["technical"]["backend_mode"], "ddp_wled");
|
||||||
|
assert_eq!(state_body["state"]["technical"]["output_enabled"], false);
|
||||||
|
assert_eq!(
|
||||||
|
state_body["state"]["technical"]["live_status"],
|
||||||
|
"DDP (WLED) selected - output disabled"
|
||||||
|
);
|
||||||
|
|
||||||
|
server.shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn discovery_scan_endpoint_returns_structured_results_and_rejects_invalid_subnets() {
|
fn discovery_scan_endpoint_returns_structured_results_and_rejects_invalid_subnets() {
|
||||||
let server = start_server();
|
let server = start_server();
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ The current delivery order is intentionally software-first:
|
|||||||
- Logic tick target: 120 Hz
|
- Logic tick target: 120 Hz
|
||||||
- Frame synthesis target: 60 Hz
|
- Frame synthesis target: 60 Hz
|
||||||
- Network send target: 40-60 Hz, profile dependent
|
- Network send target: 40-60 Hz, profile dependent
|
||||||
- Preview target: 10-15 Hz
|
- Preview target: up to 60 Hz when the active surface can render it cleanly, otherwise 30 Hz fallback
|
||||||
|
|
||||||
Preview and telemetry are explicitly degradable. Realtime output is not.
|
Preview and telemetry are explicitly degradable. Realtime output is not.
|
||||||
|
|
||||||
|
|||||||
121
docs/codex_worklog.md
Normal file
121
docs/codex_worklog.md
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
# Codex Worklog
|
||||||
|
|
||||||
|
## 2026-04-20 - Creative Surface kompakter und Preview-FPS-Pruefung
|
||||||
|
|
||||||
|
- Geaenderte Dateien:
|
||||||
|
- `web/v1/styles.css`
|
||||||
|
- `web/v1/app.js`
|
||||||
|
- `crates/infinity_host/src/runtime.rs`
|
||||||
|
- `docs/architecture.md`
|
||||||
|
- Fachliche Aenderungen:
|
||||||
|
- Creative Surface kompakter aufgestellt, damit die 18 Preview-Panels im 6x3-Raster besser vollstaendig sichtbar bleiben.
|
||||||
|
- Frontend-Preview-Drosselung von ca. 11 fps entfernt und auf eine ruhige `requestAnimationFrame`-basierte Taktung mit Host-orientiertem Ziel-FPS umgestellt.
|
||||||
|
- Host-Preview-Schedule von 15 Hz auf 60 Hz angehoben, ohne neue Architektur einzuziehen.
|
||||||
|
- Dokumentation des Preview-Ziels auf den aktuellen Stand gebracht.
|
||||||
|
- Gelaufene Tests:
|
||||||
|
- `cargo fmt --check`
|
||||||
|
- `cargo test -q -p infinity_host`
|
||||||
|
- `cargo test -q -p infinity_host_api --test contract`
|
||||||
|
- lokaler API-Check gegen temporaeren Host auf `127.0.0.1:9002` mit bestaetigtem `engine.preview_hz = 60`
|
||||||
|
- kein echter Browser-Handtest in dieser CLI-Umgebung moeglich
|
||||||
|
- Bewusste Abweichungen zur Python-Version:
|
||||||
|
- Kein Canvas-Preview-Rewrite in diesem Schritt; die bestehende DOM/CSS-Preview bleibt erhalten und wird nur verschlankt sowie render-seitig entdrosselt.
|
||||||
|
|
||||||
|
## 2026-04-20 - Diskretere Preview-Wirkung und weniger Preview-Clutter
|
||||||
|
|
||||||
|
- Geaenderte Dateien:
|
||||||
|
- `crates/infinity_host/src/scene.rs`
|
||||||
|
- `crates/infinity_host_api/src/server.rs`
|
||||||
|
- `web/v1/app.js`
|
||||||
|
- `web/v1/index.html`
|
||||||
|
- Fachliche Aenderungen:
|
||||||
|
- Normale Pattern-Intensitaet vor Preview-Ausgabe auf 10%-Stufen quantisiert.
|
||||||
|
- Normales Preview-Smoothing standardmaessig stark reduziert; nur `breathing` darf weiterhin ueber den bestehenden `fade`-Pfad weich bleiben.
|
||||||
|
- Preview-Stream im WebSocket sendet Preview nur noch bei geaendertem Payload; der bestehende Snapshot-Heartbeat bleibt fuer den API-Vertrag erhalten.
|
||||||
|
- Master Brightness in den Header verschoben; linke Brightness-Sektion zeigt nur noch pattern-spezifische Helligkeitsparameter.
|
||||||
|
- Den sichtbaren `speed`-Slider aus der Creative Surface entfernt; BPM oben plus `tempo_multiplier` bleiben die Geschwindigkeitsbedienung.
|
||||||
|
- Gelaufene Tests:
|
||||||
|
- `cargo fmt --check`
|
||||||
|
- `cargo test -q -p infinity_host`
|
||||||
|
- `cargo test -q -p infinity_host_api --test contract`
|
||||||
|
- Bewusste Abweichungen zur Python-Version:
|
||||||
|
- Keine vollstaendige Python-Pattern-Engine-Portierung; stattdessen gezielte Diskretisierung und Stream-Beruhigung innerhalb der bestehenden Rust-Host-/Web-Architektur.
|
||||||
|
- Kein echter Browser-Handtest in dieser CLI-Umgebung moeglich.
|
||||||
|
|
||||||
|
## 2026-04-20 - Creative Surface Redesign und Control-Fix (gezielt)
|
||||||
|
|
||||||
|
- Geaenderte Dateien:
|
||||||
|
- `web/v1/index.html`
|
||||||
|
- `web/v1/app.js`
|
||||||
|
- `web/v1/styles.css`
|
||||||
|
- `crates/infinity_host/src/scene.rs`
|
||||||
|
- Fachliche Aenderungen:
|
||||||
|
- Redundante globale Brightness-Steuerung in der linken Spalte entfernt; Master Brightness bleibt oben im Header.
|
||||||
|
- Sichtbaren `speed`-Slider in der Creative Surface entfernt; Bedienung ueber BPM oben plus `tempo_multiplier` in den Pattern-Parametern.
|
||||||
|
- `palette`, `color_mode`, `direction`, `mirror` als feste Controls (keine freien Textfelder) in der Web-UI beibehalten/abgesichert.
|
||||||
|
- Palette auf zwei feste Gruppen mit den vorgegebenen Namen und Hex-Werten umgestellt; Host-Palette-Mapping auf dieselben IDs umgestellt.
|
||||||
|
- Layout gezielt entkapselt: weniger harte Rahmen, leichtere Rails, kompaktere Topbar, mehr zusammenhaengende Arbeitsflaeche.
|
||||||
|
- Preview-Grid auf feste 6 Spalten gesetzt und Tiles quadratisch gemacht (`aspect-ratio: 1 / 1`) fuer ein klares 3x6-Raster.
|
||||||
|
- Gelaufene Tests:
|
||||||
|
- `cargo fmt --check`
|
||||||
|
- `cargo test -q -p infinity_host`
|
||||||
|
- `cargo test -q -p infinity_host_api --test contract`
|
||||||
|
- Browser-Handtest:
|
||||||
|
- Versuch via lokalem Headless-Firefox auf `127.0.0.1:9002` ist in dieser Umgebung fehlgeschlagen (`cannot open display: :0` / Firefox Headless-Crash).
|
||||||
|
- Bewusste Abweichungen zur Python-Version:
|
||||||
|
- Die neue, fest vorgegebene Palette-Liste ersetzt die alten Python-Palettenamen; das ist eine inhaltliche Vorgabe dieses Arbeitsschritts.
|
||||||
|
|
||||||
|
## 2026-04-20 - Palette ersetzen, linken Brightness-Slider entfernen, linke Rail entzerren
|
||||||
|
|
||||||
|
- Geaenderte Dateien:
|
||||||
|
- `web/v1/index.html`
|
||||||
|
- `web/v1/app.js`
|
||||||
|
- `web/v1/styles.css`
|
||||||
|
- Fachliche Aenderungen:
|
||||||
|
- Die Creative-Surface-Palette bleibt auf die kuratierte WS2812-Liste mit exakt vorgegebenen Namen und Hex-Werten beschraenkt.
|
||||||
|
- Den unteren Brightness-Bereich in der linken Spalte vollstaendig entfernt; globale Helligkeit bleibt ausschliesslich im Header.
|
||||||
|
- Den dynamisch aus Host-Parametern kommenden `brightness`-Control in der linken Rail ausgefiltert.
|
||||||
|
- Die linke Rail moderat verbreitert und Parameterfelder so nachgezogen, dass Dropdowns, Labels und Farbwerte weniger gequetscht sind.
|
||||||
|
- Gelaufene Tests:
|
||||||
|
- `cargo fmt --check`
|
||||||
|
- `cargo test -q -p infinity_host`
|
||||||
|
- `cargo test -q -p infinity_host_api --test contract`
|
||||||
|
- lokaler CLI-Smoke gegen temporaeren Host auf `127.0.0.1:9002` via `/api/v1/state` und `/api/v1/preview`
|
||||||
|
- Bewusste Abweichungen zur Python-Version:
|
||||||
|
- Keine; dieser Schritt korrigiert nur die Web-Darstellung und blendet den global redundanten Brightness-Slider links aus.
|
||||||
|
|
||||||
|
## 2026-04-20 - Technical Surface null-safe gegen fehlende recent_events
|
||||||
|
|
||||||
|
- Geaenderte Dateien:
|
||||||
|
- `web/v1/technical.js`
|
||||||
|
- `crates/infinity_host_api/tests/contract.rs`
|
||||||
|
- `docs/codex_worklog.md`
|
||||||
|
- Fachliche Aenderungen:
|
||||||
|
- Technical Surface gegen unvollstaendige oder startende Snapshot-Daten robuster gemacht.
|
||||||
|
- `recent_events`, `system`, `technical`, `nodes` und `panels` werden in der UI jetzt ueber null-safe Helper mit Fallbacks gelesen.
|
||||||
|
- Direkter Zugriff auf `snapshot.recent_events.map(...)` entfernt, damit `/technical` nicht mehr crasht, wenn das Feld fehlt.
|
||||||
|
- Contract-/UI-Smoke-Test ergaenzt, der absichert, dass das ausgelieferte `technical.js` den Guard fuer fehlende `recent_events` enthaelt, waehrend `/api/v1/state` das Feld weiterhin weglassen darf.
|
||||||
|
- Gelaufene Tests:
|
||||||
|
- `cargo fmt --check`
|
||||||
|
- `cargo test -q -p infinity_host_api --test contract`
|
||||||
|
- lokaler Host-Smoke auf `127.0.0.1:9011` mit erfolgreichem Abruf von `/` und `/api/v1/state`
|
||||||
|
- Bewusste Abweichungen zur Python-Version:
|
||||||
|
- Keine; das ist ein reiner Stabilitaets-/Robustheitsfix in der bestehenden Web-Oberflaeche.
|
||||||
|
|
||||||
|
## 2026-04-20 - Browser-Smoke-Runner und Output-Disable-Fix
|
||||||
|
|
||||||
|
- Geaenderte Dateien:
|
||||||
|
- `web/v1/technical.js`
|
||||||
|
- `crates/infinity_host_api/tests/contract.rs`
|
||||||
|
- `scripts/codex_browser_smoke.sh`
|
||||||
|
- `docs/codex_worklog.md`
|
||||||
|
- Fachliche Aenderungen:
|
||||||
|
- Technical-Output-Controls gegen Polling-/Apply-Rennen stabilisiert, indem waehrend `saveOutputSettings()` ein `outputSaving`-Zustand gesetzt wird.
|
||||||
|
- Output-Controls werden waehrend des Speicherns kurz deaktiviert und der Draft wird in dieser Zeit nicht von Polling-Snapshots ueberschrieben.
|
||||||
|
- Contract-Test ergaenzt, der absichert, dass `output_enabled` nach vorherigem Aktivieren wieder sauber auf `false` gesetzt werden kann.
|
||||||
|
- Kleinen lokalen Browser-/Route-Smoke-Runner fuer Codex angelegt, der eine Kurzinstanz startet und `/`, `/technical` sowie `/technical.js` selbst prueft.
|
||||||
|
- Gelaufene Tests:
|
||||||
|
- `cargo test -q -p infinity_host_api --test contract`
|
||||||
|
- `scripts/codex_browser_smoke.sh 9012`
|
||||||
|
- Bewusste Abweichungen zur Python-Version:
|
||||||
|
- Keine; der Schritt behebt einen Web-Workflow-Bug und verbessert nur den lokalen Debug-Pfad.
|
||||||
@@ -9,11 +9,27 @@ Diese Datei ist die schnelle Uebergabe fuer ein kleineres Modell wie Qwen 14B. S
|
|||||||
- Host-Core ist die zentrale Runtime und bleibt die einzige Kernarchitektur.
|
- Host-Core ist die zentrale Runtime und bleibt die einzige Kernarchitektur.
|
||||||
- API v1 ist die verbindliche Aussenkante fuer State, Preview, Snapshot, Catalog, Commands und Event-Stream.
|
- API v1 ist die verbindliche Aussenkante fuer State, Preview, Snapshot, Catalog, Commands und Event-Stream.
|
||||||
- Die Creative Surface lebt in `web/v1/` und ist die operatorische Web-Oberflaeche.
|
- Die Creative Surface lebt in `web/v1/` und ist die operatorische Web-Oberflaeche.
|
||||||
- Die Desktop-GUI in `crates/infinity_host_ui/` bleibt die technische Engineering-/Diagnoseoberflaeche.
|
- Die Technical Surface in `web/v1/technical.html` plus `web/v1/technical.js` ist die aktuelle technische Web-Oberflaeche.
|
||||||
|
- Die Desktop-GUI in `crates/infinity_host_ui/` existiert weiter als technische Engineering-/Diagnoseflaeche, ist aber nicht der primaere aktuelle UI-Arbeitspfad.
|
||||||
- Persistenz, Recovery und Runtime-Show-Store sind vorhanden.
|
- Persistenz, Recovery und Runtime-Show-Store sind vorhanden.
|
||||||
- Show-Control-v1-Primitive sind faktisch eingefroren und dokumentiert.
|
- Show-Control-v1-Primitive sind faktisch eingefroren und dokumentiert.
|
||||||
- Ein generischer externer Control-Pfad ist vorhanden, aber bewusst nicht grandMA-spezifisch.
|
- Ein generischer externer Control-Pfad ist vorhanden, aber bewusst nicht grandMA-spezifisch.
|
||||||
|
|
||||||
|
## Frisch umgesetzt in dieser Arbeitsphase
|
||||||
|
|
||||||
|
- Creative Surface wurde kompakter gemacht, damit alle 18 Panels im 3x6-Raster gleichzeitig sichtbar bleiben.
|
||||||
|
- Preview- und normale Pattern-Wirkung wurden diskreter gemacht:
|
||||||
|
- Preview-Ziel im Host liegt jetzt bei bis zu 60 Hz.
|
||||||
|
- Normale Preview-/Pattern-Helligkeit ist auf 10%-Stufen quantisiert.
|
||||||
|
- Preview-Updates und WebSocket-Preview-Frames werden nur noch bei inhaltlicher Aenderung weitergeschoben.
|
||||||
|
- Die feste WS2812-Palette wurde in Host und Web-UI hinterlegt; alte freie/abweichende Palettennamen wurden ersetzt.
|
||||||
|
- Globaler Master-Brightness sitzt nur noch im Header; redundante globale Brightness-Bedienung links ist entfernt.
|
||||||
|
- Die Technical Surface ist robuster gegen unvollstaendige Snapshots, insbesondere fehlende `recent_events`.
|
||||||
|
- Fuer Codex gibt es jetzt einen lokalen Route-/Browser-Smoke-Runner:
|
||||||
|
- `scripts/codex_browser_smoke.sh`
|
||||||
|
- prueft `/`, `/technical` und `/technical.js` gegen eine frische Kurzinstanz
|
||||||
|
- ersetzt noch keinen vollgerenderten Headless-Browser mit DOM-Interaktion
|
||||||
|
|
||||||
## Lokale Umgebung auf diesem Rechner
|
## Lokale Umgebung auf diesem Rechner
|
||||||
|
|
||||||
- Arbeitsverzeichnis des Rust-Projekts: `/home/jan/Documents/RFP/Infinity_Vis_Rust`
|
- Arbeitsverzeichnis des Rust-Projekts: `/home/jan/Documents/RFP/Infinity_Vis_Rust`
|
||||||
@@ -77,6 +93,8 @@ Die wichtigsten Fortschrittsanker liegen hier:
|
|||||||
Konflikt- und Ownership-Regeln zwischen Web-UI, technischer GUI und externen Control-Quellen.
|
Konflikt- und Ownership-Regeln zwischen Web-UI, technischer GUI und externen Control-Quellen.
|
||||||
- [docs/local_software_only_runbook.md](/home/jan/Documents/RFP/Infinity_Vis_Rust/docs/local_software_only_runbook.md:1)
|
- [docs/local_software_only_runbook.md](/home/jan/Documents/RFP/Infinity_Vis_Rust/docs/local_software_only_runbook.md:1)
|
||||||
Reproduzierbarer software-only Startpfad.
|
Reproduzierbarer software-only Startpfad.
|
||||||
|
- [docs/codex_worklog.md](/home/jan/Documents/RFP/Infinity_Vis_Rust/docs/codex_worklog.md:1)
|
||||||
|
Knapper Verlauf der letzten Codex-Arbeitspakete inklusive Tests, Abweichungen und lokalen Smoke-Checks.
|
||||||
- [crates/infinity_host/tests/show_control_v1_golden.rs](/home/jan/Documents/RFP/Infinity_Vis_Rust/crates/infinity_host/tests/show_control_v1_golden.rs:1)
|
- [crates/infinity_host/tests/show_control_v1_golden.rs](/home/jan/Documents/RFP/Infinity_Vis_Rust/crates/infinity_host/tests/show_control_v1_golden.rs:1)
|
||||||
Golden-Trace- und Replay-Schutz fuer die zentrale v1-Semantik.
|
Golden-Trace- und Replay-Schutz fuer die zentrale v1-Semantik.
|
||||||
- Git-Historie:
|
- Git-Historie:
|
||||||
@@ -129,6 +147,7 @@ Die wichtigsten Fortschrittsanker liegen hier:
|
|||||||
- [styles.css](/home/jan/Documents/RFP/Infinity_Vis_Rust/web/v1/styles.css:1)
|
- [styles.css](/home/jan/Documents/RFP/Infinity_Vis_Rust/web/v1/styles.css:1)
|
||||||
- [technical.html](/home/jan/Documents/RFP/Infinity_Vis_Rust/web/v1/technical.html:1)
|
- [technical.html](/home/jan/Documents/RFP/Infinity_Vis_Rust/web/v1/technical.html:1)
|
||||||
- [technical.js](/home/jan/Documents/RFP/Infinity_Vis_Rust/web/v1/technical.js:1)
|
- [technical.js](/home/jan/Documents/RFP/Infinity_Vis_Rust/web/v1/technical.js:1)
|
||||||
|
- [codex_browser_smoke.sh](/home/jan/Documents/RFP/Infinity_Vis_Rust/scripts/codex_browser_smoke.sh:1)
|
||||||
|
|
||||||
## Was aktuell als stabil gelten soll
|
## Was aktuell als stabil gelten soll
|
||||||
|
|
||||||
@@ -139,6 +158,7 @@ Die wichtigsten Fortschrittsanker liegen hier:
|
|||||||
- `Show/Event` bleibt staged plus Commit ueber `Go` oder `Fade Go`.
|
- `Show/Event` bleibt staged plus Commit ueber `Go` oder `Fade Go`.
|
||||||
- Preview-Only und Offline-Status ehrlich anzeigen, keine Fake-Nodes.
|
- Preview-Only und Offline-Status ehrlich anzeigen, keine Fake-Nodes.
|
||||||
- Creative Surface nicht mit technischen Mapping-Details ueberladen.
|
- Creative Surface nicht mit technischen Mapping-Details ueberladen.
|
||||||
|
- Technical Surface muss gegen partielle/startende State-Snapshots robust bleiben.
|
||||||
|
|
||||||
## Sichere Arbeitsreihenfolge fuer weitere Aenderungen
|
## Sichere Arbeitsreihenfolge fuer weitere Aenderungen
|
||||||
|
|
||||||
@@ -156,6 +176,7 @@ Die wichtigsten Fortschrittsanker liegen hier:
|
|||||||
. "$HOME/.cargo/env"
|
. "$HOME/.cargo/env"
|
||||||
cargo test -q -p infinity_host
|
cargo test -q -p infinity_host
|
||||||
cargo test -q -p infinity_host_api --test contract
|
cargo test -q -p infinity_host_api --test contract
|
||||||
|
scripts/codex_browser_smoke.sh 9012
|
||||||
```
|
```
|
||||||
|
|
||||||
Fuer lokalen software-only Start:
|
Fuer lokalen software-only Start:
|
||||||
@@ -170,6 +191,7 @@ cargo run -p infinity_host_api -- --config config/project.example.toml --bind 12
|
|||||||
- Das alte Python-Projekt ist derzeit nicht lokal neben dem Repo ausgecheckt. Fuer echte 1:1-Conformance-Vergleiche sollte es bewusst lokal daneben geklont werden.
|
- Das alte Python-Projekt ist derzeit nicht lokal neben dem Repo ausgecheckt. Fuer echte 1:1-Conformance-Vergleiche sollte es bewusst lokal daneben geklont werden.
|
||||||
- Das Runbook enthaelt noch einen veralteten Hinweis aus der frueheren Arbeitsbaum-Phase; aktuell ist das Repo wieder ein echter Git-Clone mit `.git`.
|
- Das Runbook enthaelt noch einen veralteten Hinweis aus der frueheren Arbeitsbaum-Phase; aktuell ist das Repo wieder ein echter Git-Clone mit `.git`.
|
||||||
- Die grosse Pattern-Conformance gegen das alte Python-Projekt ist noch nicht vollstaendig als separater, systematischer Abgleich abgeschlossen.
|
- Die grosse Pattern-Conformance gegen das alte Python-Projekt ist noch nicht vollstaendig als separater, systematischer Abgleich abgeschlossen.
|
||||||
|
- Ein echter gerenderter Headless-Browser-Runner mit DOM-Interaktion ist lokal noch nicht sauber eingerichtet; vorhanden ist aktuell nur der Route-/Asset-Smoke ueber `scripts/codex_browser_smoke.sh`.
|
||||||
|
|
||||||
## Kurzbriefing fuer das naechste Modell
|
## Kurzbriefing fuer das naechste Modell
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
[toolchain]
|
[toolchain]
|
||||||
channel = "stable"
|
channel = "stable"
|
||||||
components = ["rustfmt", "clippy"]
|
components = ["rustfmt", "clippy"]
|
||||||
|
|
||||||
|
|||||||
48
scripts/codex_browser_smoke.sh
Executable file
48
scripts/codex_browser_smoke.sh
Executable file
@@ -0,0 +1,48 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
REPO_DIR="/home/jan/Documents/RFP/Infinity_Vis_Rust"
|
||||||
|
PORT="${1:-9011}"
|
||||||
|
BASE_URL="http://127.0.0.1:${PORT}"
|
||||||
|
RUNTIME_STATE="/tmp/infinity_vis_runtime_codex_browser_${PORT}.json"
|
||||||
|
|
||||||
|
cd "$REPO_DIR"
|
||||||
|
. "$HOME/.cargo/env"
|
||||||
|
|
||||||
|
cargo run -q -p infinity_host_api -- \
|
||||||
|
--config config/project.example.toml \
|
||||||
|
--bind "127.0.0.1:${PORT}" \
|
||||||
|
--runtime-state "$RUNTIME_STATE" &
|
||||||
|
SERVER_PID=$!
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
if kill -0 "$SERVER_PID" >/dev/null 2>&1; then
|
||||||
|
kill "$SERVER_PID" >/dev/null 2>&1 || true
|
||||||
|
wait "$SERVER_PID" >/dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
trap cleanup EXIT INT TERM
|
||||||
|
|
||||||
|
for _attempt in $(seq 1 40); do
|
||||||
|
if curl -fsS "${BASE_URL}/api/v1/state" >/dev/null 2>&1; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 0.25
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Smoke-checking ${BASE_URL}/"
|
||||||
|
curl -fsS "${BASE_URL}/" >/tmp/infinity_vis_creative_${PORT}.html >/dev/null
|
||||||
|
echo "Smoke-checking ${BASE_URL}/technical"
|
||||||
|
curl -fsS "${BASE_URL}/technical" >/tmp/infinity_vis_technical_${PORT}.html >/dev/null
|
||||||
|
echo "Smoke-checking ${BASE_URL}/technical.js"
|
||||||
|
curl -fsS "${BASE_URL}/technical.js" >/tmp/infinity_vis_technical_${PORT}.js >/dev/null
|
||||||
|
|
||||||
|
echo "Creative Surface and Technical Surface were served successfully on ${BASE_URL}"
|
||||||
|
echo "Saved smoke artifacts to /tmp/infinity_vis_creative_${PORT}.html and /tmp/infinity_vis_technical_${PORT}.html"
|
||||||
|
|
||||||
|
if command -v xdg-open >/dev/null 2>&1; then
|
||||||
|
echo "Optional manual open:"
|
||||||
|
echo " xdg-open ${BASE_URL}/"
|
||||||
|
echo " xdg-open ${BASE_URL}/technical"
|
||||||
|
fi
|
||||||
158
web/v1/app.js
158
web/v1/app.js
@@ -1,9 +1,9 @@
|
|||||||
(function () {
|
(function () {
|
||||||
const PREVIEW_RENDER_INTERVAL_MS = 90;
|
const PREVIEW_RENDER_INTERVAL_MS = 16;
|
||||||
const STATE_RENDER_INTERVAL_MS = 180;
|
const STATE_RENDER_INTERVAL_MS = 180;
|
||||||
const POSITION_ORDER = { top: 0, middle: 1, bottom: 2 };
|
const POSITION_ORDER = { top: 0, middle: 1, bottom: 2 };
|
||||||
const COLOR_PARAM_KEYS = new Set(["color_mode", "palette", "primary_color", "secondary_color"]);
|
const COLOR_PARAM_KEYS = new Set(["color_mode", "palette", "primary_color", "secondary_color"]);
|
||||||
const BRIGHTNESS_PARAM_KEYS = new Set(["brightness"]);
|
const HIDDEN_PARAMETER_KEYS = new Set(["speed", "brightness"]);
|
||||||
const TEMPO_BPM_MIN = 10;
|
const TEMPO_BPM_MIN = 10;
|
||||||
const TEMPO_BPM_MAX = 300;
|
const TEMPO_BPM_MAX = 300;
|
||||||
const TEMPO_BPM_DEFAULT = 120;
|
const TEMPO_BPM_DEFAULT = 120;
|
||||||
@@ -46,16 +46,60 @@
|
|||||||
palette: {
|
palette: {
|
||||||
control: "enum",
|
control: "enum",
|
||||||
label: "Palette",
|
label: "Palette",
|
||||||
default_value: "Laser Club",
|
default_value: "Deep Blue",
|
||||||
|
groups: [
|
||||||
|
{
|
||||||
|
label: "White / Temperature Presets",
|
||||||
options: [
|
options: [
|
||||||
{ value: "Laser Club", label: "Laser Club" },
|
{ value: "Candle", label: "Candle (#FF9329)" },
|
||||||
{ value: "Magenta Drive", label: "Magenta Drive" },
|
{ value: "Tungsten 40W", label: "Tungsten 40W (#FFC58F)" },
|
||||||
{ value: "Warehouse Heat", label: "Warehouse Heat" },
|
{ value: "Tungsten 100W", label: "Tungsten 100W (#FFD6AA)" },
|
||||||
{ value: "UV Riot", label: "UV Riot" },
|
{ value: "Halogen", label: "Halogen (#FFF1E0)" },
|
||||||
{ value: "Redline", label: "Redline" },
|
{ value: "Carbon Arc", label: "Carbon Arc (#FFFAF4)" },
|
||||||
{ value: "Sodium Haze", label: "Sodium Haze" },
|
{ value: "High Noon Sun", label: "High Noon Sun (#FFFFFB)" },
|
||||||
{ value: "Afterhours", label: "Afterhours" },
|
{ value: "Overcast Sky", label: "Overcast Sky (#C9E2FF)" },
|
||||||
{ value: "Voltage", label: "Voltage" },
|
{ value: "Clear Blue Sky", label: "Clear Blue Sky (#409CFF)" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Club / Show Presets",
|
||||||
|
options: [
|
||||||
|
{ value: "Deep Blue", label: "Deep Blue (#0047FF)" },
|
||||||
|
{ value: "Electric Cyan", label: "Electric Cyan (#00D8FF)" },
|
||||||
|
{ value: "Emerald", label: "Emerald (#00C853)" },
|
||||||
|
{ value: "Acid Lime", label: "Acid Lime (#AEEA00)" },
|
||||||
|
{ value: "Amber", label: "Amber (#FF9D00)" },
|
||||||
|
{ value: "Signal Red", label: "Signal Red (#FF2D2D)" },
|
||||||
|
{ value: "Hot Magenta", label: "Hot Magenta (#FF00A8)" },
|
||||||
|
{ value: "Violet", label: "Violet (#7A3CFF)" },
|
||||||
|
{ value: "Warm White", label: "Warm White (#FFD6AA)" },
|
||||||
|
{ value: "Neutral White", label: "Neutral White (#FFF1E0)" },
|
||||||
|
{ value: "Cool White", label: "Cool White (#C9E2FF)" },
|
||||||
|
{ value: "Blacklight Violet", label: "Blacklight Violet (#A700FF)" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
options: [
|
||||||
|
{ value: "Candle", label: "Candle (#FF9329)" },
|
||||||
|
{ value: "Tungsten 40W", label: "Tungsten 40W (#FFC58F)" },
|
||||||
|
{ value: "Tungsten 100W", label: "Tungsten 100W (#FFD6AA)" },
|
||||||
|
{ value: "Halogen", label: "Halogen (#FFF1E0)" },
|
||||||
|
{ value: "Carbon Arc", label: "Carbon Arc (#FFFAF4)" },
|
||||||
|
{ value: "High Noon Sun", label: "High Noon Sun (#FFFFFB)" },
|
||||||
|
{ value: "Overcast Sky", label: "Overcast Sky (#C9E2FF)" },
|
||||||
|
{ value: "Clear Blue Sky", label: "Clear Blue Sky (#409CFF)" },
|
||||||
|
{ value: "Deep Blue", label: "Deep Blue (#0047FF)" },
|
||||||
|
{ value: "Electric Cyan", label: "Electric Cyan (#00D8FF)" },
|
||||||
|
{ value: "Emerald", label: "Emerald (#00C853)" },
|
||||||
|
{ value: "Acid Lime", label: "Acid Lime (#AEEA00)" },
|
||||||
|
{ value: "Amber", label: "Amber (#FF9D00)" },
|
||||||
|
{ value: "Signal Red", label: "Signal Red (#FF2D2D)" },
|
||||||
|
{ value: "Hot Magenta", label: "Hot Magenta (#FF00A8)" },
|
||||||
|
{ value: "Violet", label: "Violet (#7A3CFF)" },
|
||||||
|
{ value: "Warm White", label: "Warm White (#FFD6AA)" },
|
||||||
|
{ value: "Neutral White", label: "Neutral White (#FFF1E0)" },
|
||||||
|
{ value: "Cool White", label: "Cool White (#C9E2FF)" },
|
||||||
|
{ value: "Blacklight Violet", label: "Blacklight Violet (#A700FF)" },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
direction: {
|
direction: {
|
||||||
@@ -171,11 +215,11 @@
|
|||||||
previewLayoutSignature: null,
|
previewLayoutSignature: null,
|
||||||
viewOutputSignature: null,
|
viewOutputSignature: null,
|
||||||
stateTimer: null,
|
stateTimer: null,
|
||||||
previewTimer: null,
|
|
||||||
lastStateRenderAt: 0,
|
lastStateRenderAt: 0,
|
||||||
lastPreviewRenderAt: 0,
|
lastPreviewRenderAt: 0,
|
||||||
stateRenderQueued: false,
|
stateRenderQueued: false,
|
||||||
previewRenderQueued: false,
|
previewRenderQueued: false,
|
||||||
|
previewAnimationFrame: 0,
|
||||||
eventFilterSignature: null,
|
eventFilterSignature: null,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -225,7 +269,6 @@
|
|||||||
snapshotList: document.getElementById("snapshot-list"),
|
snapshotList: document.getElementById("snapshot-list"),
|
||||||
motionParams: document.getElementById("motion-params"),
|
motionParams: document.getElementById("motion-params"),
|
||||||
colorParams: document.getElementById("color-params"),
|
colorParams: document.getElementById("color-params"),
|
||||||
brightnessParams: document.getElementById("brightness-params"),
|
|
||||||
previewGrid: document.getElementById("preview-grid"),
|
previewGrid: document.getElementById("preview-grid"),
|
||||||
summaryCards: document.getElementById("summary-cards"),
|
summaryCards: document.getElementById("summary-cards"),
|
||||||
selectedTileCard: document.getElementById("selected-tile-card"),
|
selectedTileCard: document.getElementById("selected-tile-card"),
|
||||||
@@ -565,15 +608,28 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
apiState.ui.previewRenderQueued = true;
|
apiState.ui.previewRenderQueued = true;
|
||||||
|
const scheduleFrame = () => {
|
||||||
|
apiState.ui.previewAnimationFrame = window.requestAnimationFrame(() => {
|
||||||
const now = window.performance.now();
|
const now = window.performance.now();
|
||||||
const waitMs = Math.max(0, PREVIEW_RENDER_INTERVAL_MS - (now - apiState.ui.lastPreviewRenderAt));
|
if (now - apiState.ui.lastPreviewRenderAt < PREVIEW_RENDER_INTERVAL_MS) {
|
||||||
window.setTimeout(() => {
|
scheduleFrame();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
apiState.ui.previewAnimationFrame = 0;
|
||||||
apiState.ui.previewRenderQueued = false;
|
apiState.ui.previewRenderQueued = false;
|
||||||
apiState.ui.lastPreviewRenderAt = window.performance.now();
|
apiState.ui.lastPreviewRenderAt = now;
|
||||||
renderPreview(false);
|
renderPreview(false);
|
||||||
renderSelectedTile();
|
renderSelectedTile();
|
||||||
renderSnapshotJson();
|
renderSnapshotJson();
|
||||||
}, waitMs);
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (apiState.ui.previewAnimationFrame) {
|
||||||
|
apiState.ui.previewRenderQueued = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
scheduleFrame();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handlePrimitive(primitive, options) {
|
async function handlePrimitive(primitive, options) {
|
||||||
@@ -1120,7 +1176,6 @@
|
|||||||
if (!scene || !global) {
|
if (!scene || !global) {
|
||||||
dom.motionParams.innerHTML = "";
|
dom.motionParams.innerHTML = "";
|
||||||
dom.colorParams.innerHTML = "";
|
dom.colorParams.innerHTML = "";
|
||||||
dom.brightnessParams.innerHTML = "";
|
|
||||||
apiState.ui.parameterCards.clear();
|
apiState.ui.parameterCards.clear();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1128,13 +1183,14 @@
|
|||||||
if (
|
if (
|
||||||
!force &&
|
!force &&
|
||||||
(dom.motionParams.contains(document.activeElement) ||
|
(dom.motionParams.contains(document.activeElement) ||
|
||||||
dom.colorParams.contains(document.activeElement) ||
|
dom.colorParams.contains(document.activeElement))
|
||||||
dom.brightnessParams.contains(document.activeElement))
|
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const parameters = scene.parameters || [];
|
const parameters = (scene.parameters || []).filter(
|
||||||
|
(parameter) => !HIDDEN_PARAMETER_KEYS.has(parameter.key)
|
||||||
|
);
|
||||||
const signature = parameters
|
const signature = parameters
|
||||||
.map((parameter) => `${parameter.key}:${parameter.kind}`)
|
.map((parameter) => `${parameter.key}:${parameter.kind}`)
|
||||||
.join("|");
|
.join("|");
|
||||||
@@ -1144,7 +1200,6 @@
|
|||||||
apiState.ui.parameterCards.clear();
|
apiState.ui.parameterCards.clear();
|
||||||
dom.motionParams.innerHTML = "";
|
dom.motionParams.innerHTML = "";
|
||||||
dom.colorParams.innerHTML = "";
|
dom.colorParams.innerHTML = "";
|
||||||
dom.brightnessParams.innerHTML = "";
|
|
||||||
|
|
||||||
parameters.forEach((parameter) => {
|
parameters.forEach((parameter) => {
|
||||||
const card = createParameterCard(parameter, global);
|
const card = createParameterCard(parameter, global);
|
||||||
@@ -1165,9 +1220,6 @@
|
|||||||
if (COLOR_PARAM_KEYS.has(parameter.key)) {
|
if (COLOR_PARAM_KEYS.has(parameter.key)) {
|
||||||
return dom.colorParams;
|
return dom.colorParams;
|
||||||
}
|
}
|
||||||
if (BRIGHTNESS_PARAM_KEYS.has(parameter.key)) {
|
|
||||||
return dom.brightnessParams;
|
|
||||||
}
|
|
||||||
return dom.motionParams;
|
return dom.motionParams;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1251,6 +1303,20 @@
|
|||||||
card.appendChild(readout);
|
card.appendChild(readout);
|
||||||
} else if (controlKind === "enum") {
|
} else if (controlKind === "enum") {
|
||||||
primaryInput = document.createElement("select");
|
primaryInput = document.createElement("select");
|
||||||
|
const groupedOptions = contract && Array.isArray(contract.groups) ? contract.groups : [];
|
||||||
|
if (groupedOptions.length) {
|
||||||
|
groupedOptions.forEach((group) => {
|
||||||
|
const groupNode = document.createElement("optgroup");
|
||||||
|
groupNode.label = group.label;
|
||||||
|
group.options.forEach((option) => {
|
||||||
|
const node = document.createElement("option");
|
||||||
|
node.value = option.value;
|
||||||
|
node.textContent = option.label;
|
||||||
|
groupNode.appendChild(node);
|
||||||
|
});
|
||||||
|
primaryInput.appendChild(groupNode);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
const options = contract && Array.isArray(contract.options) ? contract.options : [];
|
const options = contract && Array.isArray(contract.options) ? contract.options : [];
|
||||||
options.forEach((option) => {
|
options.forEach((option) => {
|
||||||
const node = document.createElement("option");
|
const node = document.createElement("option");
|
||||||
@@ -1258,6 +1324,7 @@
|
|||||||
node.textContent = option.label;
|
node.textContent = option.label;
|
||||||
primaryInput.appendChild(node);
|
primaryInput.appendChild(node);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
primaryInput.addEventListener("change", async (event) => {
|
primaryInput.addEventListener("change", async (event) => {
|
||||||
const optionValue = event.target.value;
|
const optionValue = event.target.value;
|
||||||
const payloadValue =
|
const payloadValue =
|
||||||
@@ -1550,8 +1617,8 @@
|
|||||||
dom.previewUpdated.textContent = `${apiState.previewResponse.generated_at_millis} ms`;
|
dom.previewUpdated.textContent = `${apiState.previewResponse.generated_at_millis} ms`;
|
||||||
dom.previewGrid.className = `preview-grid preview-grid-mode-${apiState.ui.previewMode}`;
|
dom.previewGrid.className = `preview-grid preview-grid-mode-${apiState.ui.previewMode}`;
|
||||||
|
|
||||||
const columnCount = uniqueNodeIds(panels).length;
|
const columnCount = 6;
|
||||||
dom.previewGrid.style.gridTemplateColumns = `repeat(${columnCount}, minmax(112px, 1fr))`;
|
dom.previewGrid.style.gridTemplateColumns = `repeat(${columnCount}, minmax(0, 1fr))`;
|
||||||
|
|
||||||
const layoutSignature = panels.map((panel) => panel.key).join("|");
|
const layoutSignature = panels.map((panel) => panel.key).join("|");
|
||||||
if (force || layoutSignature !== apiState.ui.previewLayoutSignature) {
|
if (force || layoutSignature !== apiState.ui.previewLayoutSignature) {
|
||||||
@@ -2437,40 +2504,16 @@
|
|||||||
const sampleColors = (panel.sample_led_hex || [])
|
const sampleColors = (panel.sample_led_hex || [])
|
||||||
.map((hex) => normalizeColorHex(hex))
|
.map((hex) => normalizeColorHex(hex))
|
||||||
.filter((hex) => isHexColorString(hex));
|
.filter((hex) => isHexColorString(hex));
|
||||||
const palette = sampleColors.length
|
if (!sampleColors.length) {
|
||||||
? sampleColors
|
return TILE_LED_GEOMETRY.map(() => normalizeColorHex(panel.representative_color_hex));
|
||||||
: [normalizeColorHex(panel.representative_color_hex)];
|
|
||||||
|
|
||||||
if (palette.length === 1) {
|
|
||||||
return TILE_LED_GEOMETRY.map(() => palette[0]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return TILE_LED_GEOMETRY.map((_led, index) => {
|
return TILE_LED_GEOMETRY.map((_led, index) => {
|
||||||
const factor = TILE_LED_GEOMETRY.length === 1 ? 0 : index / (TILE_LED_GEOMETRY.length - 1);
|
const sourceIndex = Math.round((index / Math.max(1, TILE_LED_GEOMETRY.length - 1)) * (sampleColors.length - 1));
|
||||||
return interpolatePalette(palette, factor);
|
return sampleColors[sourceIndex] || normalizeColorHex(panel.representative_color_hex);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function interpolatePalette(palette, factor) {
|
|
||||||
const scaled = factor * (palette.length - 1);
|
|
||||||
const lowerIndex = Math.floor(scaled);
|
|
||||||
const upperIndex = Math.min(palette.length - 1, Math.ceil(scaled));
|
|
||||||
if (lowerIndex === upperIndex) {
|
|
||||||
return palette[lowerIndex];
|
|
||||||
}
|
|
||||||
return mixHexColors(palette[lowerIndex], palette[upperIndex], scaled - lowerIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
function mixHexColors(leftHex, rightHex, factor) {
|
|
||||||
const left = parseHexColor(leftHex);
|
|
||||||
const right = parseHexColor(rightHex);
|
|
||||||
return rgbToHex(
|
|
||||||
Math.round(lerp(left.r, right.r, factor)),
|
|
||||||
Math.round(lerp(left.g, right.g, factor)),
|
|
||||||
Math.round(lerp(left.b, right.b, factor))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseHexColor(hex) {
|
function parseHexColor(hex) {
|
||||||
const normalized = normalizeColorHex(hex).slice(1);
|
const normalized = normalizeColorHex(hex).slice(1);
|
||||||
return {
|
return {
|
||||||
@@ -2486,9 +2529,10 @@
|
|||||||
|
|
||||||
function previewLedOpacity(energyPercent) {
|
function previewLedOpacity(energyPercent) {
|
||||||
if (!energyPercent) {
|
if (!energyPercent) {
|
||||||
return "0.18";
|
return "0.1";
|
||||||
}
|
}
|
||||||
return String(Math.min(1, 0.42 + energyPercent / 160));
|
const steppedPercent = Math.round(Math.max(0, Math.min(100, energyPercent)) / 10) * 10;
|
||||||
|
return String(Math.max(0.1, steppedPercent / 100));
|
||||||
}
|
}
|
||||||
|
|
||||||
function escapeHtml(value) {
|
function escapeHtml(value) {
|
||||||
|
|||||||
@@ -26,6 +26,12 @@
|
|||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
<label class="toolbar-control toolbar-control-inline">
|
||||||
|
<span class="toolbar-label">Master</span>
|
||||||
|
<input id="brightness-slider" type="range" min="0" max="1" step="0.01" />
|
||||||
|
<strong id="brightness-value">0%</strong>
|
||||||
|
</label>
|
||||||
|
|
||||||
<button id="go-button" class="toolbar-button" type="button">Go</button>
|
<button id="go-button" class="toolbar-button" type="button">Go</button>
|
||||||
<button id="fade-go-button" class="toolbar-button toolbar-button-primary" type="button">
|
<button id="fade-go-button" class="toolbar-button toolbar-button-primary" type="button">
|
||||||
Fade Go
|
Fade Go
|
||||||
@@ -83,18 +89,6 @@
|
|||||||
<div id="color-params" class="parameter-stack"></div>
|
<div id="color-params" class="parameter-stack"></div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="dock-section">
|
|
||||||
<div class="dock-header">
|
|
||||||
<h2>Brightness</h2>
|
|
||||||
<p>Global intensity plus pattern-level brightness controls.</p>
|
|
||||||
</div>
|
|
||||||
<label class="control-field">
|
|
||||||
<span>Master Brightness</span>
|
|
||||||
<input id="brightness-slider" type="range" min="0" max="1" step="0.01" />
|
|
||||||
<strong id="brightness-value">0%</strong>
|
|
||||||
</label>
|
|
||||||
<div id="brightness-params" class="parameter-stack"></div>
|
|
||||||
</section>
|
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<section class="workspace-stage">
|
<section class="workspace-stage">
|
||||||
|
|||||||
@@ -1,25 +1,25 @@
|
|||||||
:root {
|
:root {
|
||||||
color-scheme: dark;
|
color-scheme: dark;
|
||||||
--bg: #1e1e1e;
|
--bg: #151920;
|
||||||
--bg-elevated: #252526;
|
--bg-elevated: #1b212c;
|
||||||
--bg-elevated-2: #2d2d30;
|
--bg-elevated-2: #202838;
|
||||||
--bg-stage: #0d1016;
|
--bg-stage: #0d1016;
|
||||||
--bg-stage-2: #11151d;
|
--bg-stage-2: #11151d;
|
||||||
--line: #3c3c3c;
|
--line: rgba(182, 202, 227, 0.14);
|
||||||
--line-soft: #2f2f33;
|
--line-soft: rgba(182, 202, 227, 0.08);
|
||||||
--text: #cccccc;
|
--text: #c9d2df;
|
||||||
--text-strong: #f3f6fb;
|
--text-strong: #f3f6fb;
|
||||||
--muted: #8f99a5;
|
--muted: #95a1b2;
|
||||||
--accent: #007acc;
|
--accent: #007acc;
|
||||||
--accent-strong: #094771;
|
--accent-strong: #0a4f8d;
|
||||||
--accent-soft: rgba(0, 122, 204, 0.18);
|
--accent-soft: rgba(0, 122, 204, 0.18);
|
||||||
--warning: #d6a04d;
|
--warning: #d6a04d;
|
||||||
--danger: #c63b1e;
|
--danger: #c63b1e;
|
||||||
--danger-soft: rgba(198, 59, 30, 0.18);
|
--danger-soft: rgba(198, 59, 30, 0.18);
|
||||||
--success: #1f8b63;
|
--success: #1f8b63;
|
||||||
--success-soft: rgba(31, 139, 99, 0.18);
|
--success-soft: rgba(31, 139, 99, 0.18);
|
||||||
--shadow: 0 18px 48px rgba(0, 0, 0, 0.34);
|
--shadow: 0 10px 26px rgba(0, 0, 0, 0.2);
|
||||||
--tile-shadow: 0 14px 34px rgba(0, 0, 0, 0.38);
|
--tile-shadow: 0 8px 18px rgba(0, 0, 0, 0.22);
|
||||||
--radius-sm: 3px;
|
--radius-sm: 3px;
|
||||||
--radius-md: 4px;
|
--radius-md: 4px;
|
||||||
--radius-lg: 6px;
|
--radius-lg: 6px;
|
||||||
@@ -110,26 +110,26 @@ input[type="range"] {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
padding: 6px 10px;
|
padding: 3px 7px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.topbar-creative .topbar-actions {
|
.topbar-creative .topbar-actions {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 6px;
|
gap: 3px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.topbar-creative .toolbar-control,
|
.topbar-creative .toolbar-control,
|
||||||
.topbar-creative .toolbar-group,
|
.topbar-creative .toolbar-group,
|
||||||
.topbar-creative .toolbar-button {
|
.topbar-creative .toolbar-button {
|
||||||
min-height: 30px;
|
min-height: 26px;
|
||||||
padding: 4px 8px;
|
padding: 2px 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.topbar-creative .toolbar-control-fade {
|
.topbar-creative .toolbar-control-fade {
|
||||||
min-width: 222px;
|
min-width: 184px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.topbar-brand,
|
.topbar-brand,
|
||||||
@@ -166,10 +166,10 @@ input[type="range"] {
|
|||||||
.toolbar-control {
|
.toolbar-control {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 6px;
|
||||||
min-height: 34px;
|
min-height: 28px;
|
||||||
padding: 6px 8px;
|
padding: 3px 6px;
|
||||||
background: #1f1f1f;
|
background: rgba(17, 22, 31, 0.72);
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,13 +178,13 @@ input[type="range"] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.toolbar-control-fade {
|
.toolbar-control-fade {
|
||||||
min-width: 250px;
|
min-width: 192px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#tempo-bpm-input,
|
#tempo-bpm-input,
|
||||||
#transition-seconds-input {
|
#transition-seconds-input {
|
||||||
width: 82px;
|
width: 68px;
|
||||||
min-width: 82px;
|
min-width: 68px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toolbar-label {
|
.toolbar-label {
|
||||||
@@ -198,10 +198,10 @@ input[type="range"] {
|
|||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
min-width: 100px;
|
min-width: 88px;
|
||||||
min-height: 24px;
|
min-height: 22px;
|
||||||
padding: 4px 10px;
|
padding: 3px 8px;
|
||||||
font-size: 0.77rem;
|
font-size: 0.72rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.06em;
|
letter-spacing: 0.06em;
|
||||||
@@ -245,9 +245,9 @@ input[type="range"] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.toolbar-button {
|
.toolbar-button {
|
||||||
min-height: 34px;
|
min-height: 28px;
|
||||||
padding: 7px 14px;
|
padding: 4px 8px;
|
||||||
background: var(--bg-elevated-2);
|
background: rgba(17, 22, 31, 0.74);
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
}
|
}
|
||||||
@@ -287,9 +287,9 @@ a.toolbar-button {
|
|||||||
|
|
||||||
.workspace {
|
.workspace {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 270px minmax(0, 1fr) 310px;
|
grid-template-columns: 252px minmax(0, 1fr) 236px;
|
||||||
gap: 8px;
|
gap: 4px;
|
||||||
padding: 8px;
|
padding: 4px;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -300,7 +300,7 @@ a.toolbar-button {
|
|||||||
|
|
||||||
.workspace-rail {
|
.workspace-rail {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 8px;
|
gap: 4px;
|
||||||
align-content: start;
|
align-content: start;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
padding-right: 2px;
|
padding-right: 2px;
|
||||||
@@ -308,27 +308,29 @@ a.toolbar-button {
|
|||||||
|
|
||||||
.workspace-stage {
|
.workspace-stage {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-rows: minmax(0, 1fr) 220px;
|
grid-template-rows: minmax(0, 1fr) 148px;
|
||||||
gap: 8px;
|
gap: 4px;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stage-panel,
|
.stage-panel,
|
||||||
.dock-section {
|
.dock-section {
|
||||||
background: var(--bg-elevated);
|
background: var(--bg-elevated);
|
||||||
border: 1px solid var(--line);
|
border: 1px solid transparent;
|
||||||
box-shadow: var(--shadow);
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dock-section {
|
.dock-section {
|
||||||
padding: 8px;
|
padding: 5px;
|
||||||
|
background: color-mix(in srgb, var(--bg-elevated) 86%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.stage-panel {
|
.stage-panel {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 10px;
|
gap: 6px;
|
||||||
padding: 8px;
|
padding: 5px;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
|
background: color-mix(in srgb, var(--bg-elevated-2) 74%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.stage-panel-preview {
|
.stage-panel-preview {
|
||||||
@@ -343,14 +345,14 @@ a.toolbar-button {
|
|||||||
.stage-header {
|
.stage-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 12px;
|
gap: 8px;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dock-header h2,
|
.dock-header h2,
|
||||||
.stage-header h2 {
|
.stage-header h2 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 0.96rem;
|
font-size: 0.84rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: var(--text-strong);
|
color: var(--text-strong);
|
||||||
}
|
}
|
||||||
@@ -361,8 +363,8 @@ a.toolbar-button {
|
|||||||
.panel-meta,
|
.panel-meta,
|
||||||
.info-detail,
|
.info-detail,
|
||||||
.event-meta {
|
.event-meta {
|
||||||
margin: 3px 0 0;
|
margin: 1px 0 0;
|
||||||
font-size: 0.78rem;
|
font-size: 0.68rem;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -385,12 +387,12 @@ a.toolbar-button {
|
|||||||
.compact-form-two,
|
.compact-form-two,
|
||||||
.parameter-card label {
|
.parameter-card label {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 8px;
|
gap: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.control-field span,
|
.control-field span,
|
||||||
.parameter-card span {
|
.parameter-card span {
|
||||||
font-size: 0.78rem;
|
font-size: 0.72rem;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -408,13 +410,13 @@ a.toolbar-button {
|
|||||||
.summary-cards,
|
.summary-cards,
|
||||||
.info-list {
|
.info-list {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 8px;
|
gap: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button-stack,
|
.button-stack,
|
||||||
.button-row {
|
.button-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 6px;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -423,7 +425,7 @@ a.toolbar-button {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.compact-form {
|
.compact-form {
|
||||||
margin-top: 10px;
|
margin-top: 8px;
|
||||||
grid-template-columns: minmax(0, 1fr) auto;
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
@@ -433,7 +435,7 @@ a.toolbar-button {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.compact-form-two {
|
.compact-form-two {
|
||||||
margin-top: 10px;
|
margin-top: 8px;
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
@@ -442,7 +444,7 @@ a.toolbar-button {
|
|||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
min-height: 34px;
|
min-height: 30px;
|
||||||
padding: 0 6px;
|
padding: 0 6px;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
}
|
}
|
||||||
@@ -450,28 +452,28 @@ a.toolbar-button {
|
|||||||
.pending-status-row {
|
.pending-status-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
gap: 8px;
|
gap: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mini-status {
|
.mini-status {
|
||||||
padding: 8px 10px;
|
padding: 6px 8px;
|
||||||
background: #1f1f1f;
|
background: #1f1f1f;
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
}
|
}
|
||||||
|
|
||||||
.preview-stage {
|
.preview-stage {
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
padding: 10px 10px;
|
padding: 4px;
|
||||||
background:
|
background:
|
||||||
linear-gradient(180deg, rgba(255, 255, 255, 0.02), transparent 14%),
|
linear-gradient(180deg, rgba(255, 255, 255, 0.02), transparent 14%),
|
||||||
linear-gradient(180deg, var(--bg-stage) 0%, var(--bg-stage-2) 100%);
|
linear-gradient(180deg, var(--bg-stage) 0%, var(--bg-stage-2) 100%);
|
||||||
border: 1px solid #20252f;
|
border: 1px solid rgba(182, 202, 227, 0.08);
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.preview-grid {
|
.preview-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 10px;
|
gap: 4px;
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
align-content: center;
|
align-content: center;
|
||||||
}
|
}
|
||||||
@@ -479,12 +481,13 @@ a.toolbar-button {
|
|||||||
.preview-tile {
|
.preview-tile {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: block;
|
display: block;
|
||||||
min-height: 168px;
|
aspect-ratio: 1 / 1;
|
||||||
padding: 8px;
|
min-height: 0;
|
||||||
background: linear-gradient(180deg, #1b1b1b 0%, #111318 100%);
|
padding: 4px;
|
||||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
background: linear-gradient(180deg, #151a24 0%, #10141d 100%);
|
||||||
|
border: 1px solid rgba(182, 202, 227, 0.12);
|
||||||
color: var(--text-strong);
|
color: var(--text-strong);
|
||||||
box-shadow: var(--tile-shadow);
|
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.2);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -492,11 +495,11 @@ a.toolbar-button {
|
|||||||
content: "";
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: auto -15% -40% auto;
|
inset: auto -15% -40% auto;
|
||||||
width: 62%;
|
width: 52%;
|
||||||
height: 62%;
|
height: 52%;
|
||||||
background: radial-gradient(circle, var(--tile-glow, #4d7cff), transparent 72%);
|
background: radial-gradient(circle, var(--tile-glow, #4d7cff), transparent 72%);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
opacity: 0.16;
|
opacity: 0.08;
|
||||||
}
|
}
|
||||||
|
|
||||||
.preview-tile:hover {
|
.preview-tile:hover {
|
||||||
@@ -504,9 +507,9 @@ a.toolbar-button {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.preview-tile.is-selected {
|
.preview-tile.is-selected {
|
||||||
outline: 2px solid var(--accent);
|
outline: 1px solid var(--accent);
|
||||||
outline-offset: -2px;
|
outline-offset: -1px;
|
||||||
border-color: rgba(0, 122, 204, 0.7);
|
border-color: rgba(0, 122, 204, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
.preview-tile.is-panel-test {
|
.preview-tile.is-panel-test {
|
||||||
@@ -520,12 +523,12 @@ a.toolbar-button {
|
|||||||
.tile-preview-shell {
|
.tile-preview-shell {
|
||||||
position: relative;
|
position: relative;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
min-height: 148px;
|
min-height: 0;
|
||||||
border-radius: 4px;
|
border-radius: 3px;
|
||||||
background:
|
background:
|
||||||
linear-gradient(180deg, rgba(255, 255, 255, 0.02), transparent 16%),
|
linear-gradient(180deg, rgba(255, 255, 255, 0.02), transparent 16%),
|
||||||
linear-gradient(180deg, #060912 0%, #080d18 100%);
|
linear-gradient(180deg, #060912 0%, #080d18 100%);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.03);
|
border: 1px solid rgba(182, 202, 227, 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tile-led-ring {
|
.tile-led-ring {
|
||||||
@@ -535,10 +538,10 @@ a.toolbar-button {
|
|||||||
|
|
||||||
.tile-led {
|
.tile-led {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 4px;
|
width: 3px;
|
||||||
height: 4px;
|
height: 3px;
|
||||||
margin-left: -2px;
|
margin-left: -1.5px;
|
||||||
margin-top: -2px;
|
margin-top: -1.5px;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
background: var(--tile-color, #4d7cff);
|
background: var(--tile-color, #4d7cff);
|
||||||
opacity: var(--led-opacity, 0.85);
|
opacity: var(--led-opacity, 0.85);
|
||||||
@@ -546,16 +549,16 @@ a.toolbar-button {
|
|||||||
|
|
||||||
.tile-overlay {
|
.tile-overlay {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 10px;
|
inset: 4px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 14px 10px 12px;
|
padding: 5px 4px 4px;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tile-label {
|
.tile-label {
|
||||||
font-size: clamp(1.15rem, 1vw + 0.8rem, 2rem);
|
font-size: clamp(0.72rem, 0.32vw + 0.62rem, 1rem);
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
color: rgba(237, 243, 255, 0.94);
|
color: rgba(237, 243, 255, 0.94);
|
||||||
@@ -563,7 +566,7 @@ a.toolbar-button {
|
|||||||
|
|
||||||
.tile-caption {
|
.tile-caption {
|
||||||
margin-top: auto;
|
margin-top: auto;
|
||||||
font-size: 1rem;
|
font-size: 0.68rem;
|
||||||
color: rgba(214, 224, 238, 0.76);
|
color: rgba(214, 224, 238, 0.76);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -766,9 +769,9 @@ a.toolbar-button {
|
|||||||
.tile-meta {
|
.tile-meta {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 10px;
|
gap: 8px;
|
||||||
align-items: end;
|
align-items: end;
|
||||||
font-size: 0.74rem;
|
font-size: 0.58rem;
|
||||||
color: rgba(214, 224, 238, 0.64);
|
color: rgba(214, 224, 238, 0.64);
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.05em;
|
letter-spacing: 0.05em;
|
||||||
@@ -803,10 +806,10 @@ a.toolbar-button {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.preview-grid-mode-leds .tile-led {
|
.preview-grid-mode-leds .tile-led {
|
||||||
width: 4.6px;
|
width: 3.4px;
|
||||||
height: 4.6px;
|
height: 3.4px;
|
||||||
margin-left: -2.3px;
|
margin-left: -1.7px;
|
||||||
margin-top: -2.3px;
|
margin-top: -1.7px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.preview-grid-mode-tile .tile-preview-shell {
|
.preview-grid-mode-tile .tile-preview-shell {
|
||||||
@@ -826,9 +829,9 @@ a.toolbar-button {
|
|||||||
.event-item,
|
.event-item,
|
||||||
.selected-tile-card,
|
.selected-tile-card,
|
||||||
.info-row {
|
.info-row {
|
||||||
padding: 10px;
|
padding: 6px;
|
||||||
background: #1f1f1f;
|
background: rgba(17, 22, 31, 0.7);
|
||||||
border: 1px solid var(--line);
|
border: 1px solid rgba(182, 202, 227, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.summary-card span {
|
.summary-card span {
|
||||||
@@ -861,7 +864,7 @@ a.toolbar-button {
|
|||||||
|
|
||||||
.list-item-meta {
|
.list-item-meta {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 6px;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
font-size: 0.76rem;
|
font-size: 0.76rem;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
@@ -879,7 +882,7 @@ a.toolbar-button {
|
|||||||
|
|
||||||
.parameter-card {
|
.parameter-card {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 8px;
|
gap: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.parameter-card.is-staged {
|
.parameter-card.is-staged {
|
||||||
@@ -891,12 +894,20 @@ a.toolbar-button {
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.parameter-key {
|
.parameter-key {
|
||||||
font-size: 0.72rem;
|
font-size: 0.72rem;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.05em;
|
letter-spacing: 0.05em;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.parameter-card select,
|
||||||
|
.parameter-card input[type="text"],
|
||||||
|
.parameter-card input[type="number"] {
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.parameter-readout {
|
.parameter-readout {
|
||||||
@@ -906,13 +917,13 @@ a.toolbar-button {
|
|||||||
|
|
||||||
.color-input-row {
|
.color-input-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 44px minmax(0, 1fr);
|
grid-template-columns: 50px minmax(0, 1fr);
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.color-input-row input[type="color"] {
|
.color-input-row input[type="color"] {
|
||||||
width: 44px;
|
width: 50px;
|
||||||
height: 34px;
|
height: 34px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
@@ -921,11 +932,11 @@ a.toolbar-button {
|
|||||||
|
|
||||||
.selected-tile-card {
|
.selected-tile-card {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 8px;
|
gap: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.selected-tile-title {
|
.selected-tile-title {
|
||||||
font-size: 1.08rem;
|
font-size: 0.98rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: var(--text-strong);
|
color: var(--text-strong);
|
||||||
}
|
}
|
||||||
@@ -933,13 +944,13 @@ a.toolbar-button {
|
|||||||
.selected-tile-grid {
|
.selected-tile-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
gap: 8px;
|
gap: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-row {
|
.info-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
padding: 8px 10px;
|
padding: 6px 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-label {
|
.info-label {
|
||||||
@@ -959,7 +970,7 @@ a.toolbar-button {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.pending-card {
|
.pending-card {
|
||||||
padding: 10px;
|
padding: 8px;
|
||||||
background: rgba(214, 160, 77, 0.08);
|
background: rgba(214, 160, 77, 0.08);
|
||||||
border: 1px solid rgba(214, 160, 77, 0.2);
|
border: 1px solid rgba(214, 160, 77, 0.2);
|
||||||
}
|
}
|
||||||
@@ -971,7 +982,7 @@ a.toolbar-button {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.primitive-error-banner {
|
.primitive-error-banner {
|
||||||
padding: 10px;
|
padding: 8px;
|
||||||
background: var(--danger-soft);
|
background: var(--danger-soft);
|
||||||
border: 1px solid rgba(198, 59, 30, 0.4);
|
border: 1px solid rgba(198, 59, 30, 0.4);
|
||||||
color: #ffb09e;
|
color: #ffb09e;
|
||||||
@@ -984,8 +995,8 @@ a.toolbar-button {
|
|||||||
|
|
||||||
.event-filter-bar {
|
.event-filter-bar {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 170px minmax(0, 1fr);
|
grid-template-columns: 132px minmax(0, 1fr);
|
||||||
gap: 8px;
|
gap: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.event-list {
|
.event-list {
|
||||||
@@ -995,7 +1006,7 @@ a.toolbar-button {
|
|||||||
|
|
||||||
.event-item {
|
.event-item {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 6px;
|
gap: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.event-item strong {
|
.event-item strong {
|
||||||
@@ -1038,9 +1049,9 @@ a.toolbar-button {
|
|||||||
|
|
||||||
.snapshot-json {
|
.snapshot-json {
|
||||||
margin: 8px 0 0;
|
margin: 8px 0 0;
|
||||||
max-height: 260px;
|
max-height: 170px;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
padding: 10px;
|
padding: 8px;
|
||||||
background: #111317;
|
background: #111317;
|
||||||
border: 1px solid #20252f;
|
border: 1px solid #20252f;
|
||||||
color: #dde7f5;
|
color: #dde7f5;
|
||||||
@@ -1053,8 +1064,8 @@ a.toolbar-button {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.empty-state {
|
.empty-state {
|
||||||
padding: 12px;
|
padding: 8px;
|
||||||
border: 1px dashed var(--line);
|
border: 1px dashed rgba(182, 202, 227, 0.2);
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1062,13 +1073,18 @@ a.toolbar-button {
|
|||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
select optgroup {
|
||||||
|
background: #141923;
|
||||||
|
color: #cdd7e5;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 1460px) {
|
@media (max-width: 1460px) {
|
||||||
.topbar {
|
.topbar {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
.workspace {
|
.workspace {
|
||||||
grid-template-columns: 248px minmax(0, 1fr) 280px;
|
grid-template-columns: 192px minmax(0, 1fr) 212px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.summary-cards {
|
.summary-cards {
|
||||||
@@ -1091,7 +1107,7 @@ a.toolbar-button {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.workspace-stage {
|
.workspace-stage {
|
||||||
grid-template-rows: minmax(480px, auto) auto;
|
grid-template-rows: minmax(360px, auto) auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.workspace-rail,
|
.workspace-rail,
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ const appState = {
|
|||||||
snapshot: null,
|
snapshot: null,
|
||||||
outputDraft: null,
|
outputDraft: null,
|
||||||
outputDirty: false,
|
outputDirty: false,
|
||||||
|
outputSaving: false,
|
||||||
nodeDrafts: new Map(),
|
nodeDrafts: new Map(),
|
||||||
panelDrafts: new Map(),
|
panelDrafts: new Map(),
|
||||||
discovery: {
|
discovery: {
|
||||||
@@ -181,17 +182,22 @@ async function loadState() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function syncDraftsFromState(snapshot) {
|
function syncDraftsFromState(snapshot) {
|
||||||
elements.projectName.textContent = `${snapshot.system.project_name} | ${snapshot.system.topology_label}`;
|
const system = snapshotSystem(snapshot);
|
||||||
|
const technical = snapshotTechnical(snapshot);
|
||||||
|
const nodes = snapshotNodes(snapshot);
|
||||||
|
const panels = snapshotPanels(snapshot);
|
||||||
|
|
||||||
if (!appState.outputDirty) {
|
elements.projectName.textContent = `${system.project_name} | ${system.topology_label}`;
|
||||||
|
|
||||||
|
if (!appState.outputDirty && !appState.outputSaving) {
|
||||||
appState.outputDraft = {
|
appState.outputDraft = {
|
||||||
backend_mode: snapshot.technical.backend_mode,
|
backend_mode: technical.backend_mode,
|
||||||
output_enabled: snapshot.technical.output_enabled,
|
output_enabled: technical.output_enabled,
|
||||||
output_fps: snapshot.technical.output_fps,
|
output_fps: technical.output_fps,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const node of snapshot.nodes) {
|
for (const node of nodes) {
|
||||||
const existing = appState.nodeDrafts.get(node.node_id);
|
const existing = appState.nodeDrafts.get(node.node_id);
|
||||||
if (!existing || !existing.dirty) {
|
if (!existing || !existing.dirty) {
|
||||||
appState.nodeDrafts.set(node.node_id, {
|
appState.nodeDrafts.set(node.node_id, {
|
||||||
@@ -201,7 +207,7 @@ function syncDraftsFromState(snapshot) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const panel of snapshot.panels) {
|
for (const panel of panels) {
|
||||||
const key = panelKey(panel.node_id, panel.panel_position);
|
const key = panelKey(panel.node_id, panel.panel_position);
|
||||||
const existing = appState.panelDrafts.get(key);
|
const existing = appState.panelDrafts.get(key);
|
||||||
if (!existing || !existing.dirty) {
|
if (!existing || !existing.dirty) {
|
||||||
@@ -255,20 +261,22 @@ function renderTopbar() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const onlineCount = snapshot.nodes.filter((node) => node.connection === "online").length;
|
const technical = snapshotTechnical(snapshot);
|
||||||
|
const nodes = snapshotNodes(snapshot);
|
||||||
|
const onlineCount = nodes.filter((node) => node.connection === "online").length;
|
||||||
setChip(
|
setChip(
|
||||||
elements.backendPill,
|
elements.backendPill,
|
||||||
backendModeLabel(snapshot.technical.backend_mode),
|
backendModeLabel(technical.backend_mode),
|
||||||
snapshot.technical.backend_mode === "preview_only" ? "idle" : "live"
|
technical.backend_mode === "preview_only" ? "idle" : "live"
|
||||||
);
|
);
|
||||||
setChip(
|
setChip(
|
||||||
elements.outputPill,
|
elements.outputPill,
|
||||||
snapshot.technical.output_enabled ? "enabled" : "disabled",
|
technical.output_enabled ? "enabled" : "disabled",
|
||||||
snapshot.technical.output_enabled ? "success" : "warning"
|
technical.output_enabled ? "success" : "warning"
|
||||||
);
|
);
|
||||||
setChip(
|
setChip(
|
||||||
elements.nodesPill,
|
elements.nodesPill,
|
||||||
`${onlineCount}/${snapshot.nodes.length} online`,
|
`${onlineCount}/${nodes.length} online`,
|
||||||
onlineCount > 0 ? "success" : "warning"
|
onlineCount > 0 ? "success" : "warning"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -278,7 +286,9 @@ function renderSummaryCards() {
|
|||||||
elements.summaryGrid.innerHTML = "";
|
elements.summaryGrid.innerHTML = "";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const { technical, nodes, panels } = appState.snapshot;
|
const technical = snapshotTechnical(appState.snapshot);
|
||||||
|
const nodes = snapshotNodes(appState.snapshot);
|
||||||
|
const panels = snapshotPanels(appState.snapshot);
|
||||||
const onlineCount = nodes.filter((node) => node.connection === "online").length;
|
const onlineCount = nodes.filter((node) => node.connection === "online").length;
|
||||||
const enabledOutputs = panels.filter((panel) => panel.enabled).length;
|
const enabledOutputs = panels.filter((panel) => panel.enabled).length;
|
||||||
const liveOutputs = panels.filter(
|
const liveOutputs = panels.filter(
|
||||||
@@ -313,10 +323,15 @@ function renderOutputControls() {
|
|||||||
elements.backendModeSelect.value = appState.outputDraft.backend_mode;
|
elements.backendModeSelect.value = appState.outputDraft.backend_mode;
|
||||||
elements.outputEnabledInput.checked = Boolean(appState.outputDraft.output_enabled);
|
elements.outputEnabledInput.checked = Boolean(appState.outputDraft.output_enabled);
|
||||||
elements.outputFpsInput.value = String(appState.outputDraft.output_fps);
|
elements.outputFpsInput.value = String(appState.outputDraft.output_fps);
|
||||||
elements.liveStatus.textContent = appState.snapshot.technical.live_status;
|
const technical = snapshotTechnical(appState.snapshot);
|
||||||
elements.liveStatus.className = `status-banner ${bannerLevelForTechnical(appState.snapshot.technical)}`;
|
elements.liveStatus.textContent = technical.live_status;
|
||||||
|
elements.liveStatus.className = `status-banner ${bannerLevelForTechnical(technical)}`;
|
||||||
elements.backendSemantics.textContent = backendSemanticsText(appState.outputDraft);
|
elements.backendSemantics.textContent = backendSemanticsText(appState.outputDraft);
|
||||||
elements.saveOutputSettingsButton.disabled = !appState.outputDirty;
|
const controlsDisabled = appState.outputSaving;
|
||||||
|
elements.backendModeSelect.disabled = controlsDisabled;
|
||||||
|
elements.outputEnabledInput.disabled = controlsDisabled;
|
||||||
|
elements.outputFpsInput.disabled = controlsDisabled;
|
||||||
|
elements.saveOutputSettingsButton.disabled = controlsDisabled || !appState.outputDirty;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderNodeTable() {
|
function renderNodeTable() {
|
||||||
@@ -325,7 +340,7 @@ function renderNodeTable() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
elements.nodeTableBody.innerHTML = appState.snapshot.nodes
|
elements.nodeTableBody.innerHTML = snapshotNodes(appState.snapshot)
|
||||||
.map((node) => {
|
.map((node) => {
|
||||||
const draft = ensureNodeDraft(node.node_id);
|
const draft = ensureNodeDraft(node.node_id);
|
||||||
return `
|
return `
|
||||||
@@ -368,7 +383,7 @@ function renderPanelTable() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
elements.panelTableBody.innerHTML = appState.snapshot.panels
|
elements.panelTableBody.innerHTML = snapshotPanels(appState.snapshot)
|
||||||
.map((panel) => {
|
.map((panel) => {
|
||||||
const key = panelKey(panel.node_id, panel.panel_position);
|
const key = panelKey(panel.node_id, panel.panel_position);
|
||||||
const draft = ensurePanelDraft(key, panel);
|
const draft = ensurePanelDraft(key, panel);
|
||||||
@@ -505,7 +520,8 @@ function renderEvents() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
elements.eventList.innerHTML = appState.snapshot.recent_events
|
const recentEvents = snapshotRecentEvents(appState.snapshot);
|
||||||
|
elements.eventList.innerHTML = recentEvents
|
||||||
.map(
|
.map(
|
||||||
(event) => `
|
(event) => `
|
||||||
<article class="event-entry">
|
<article class="event-entry">
|
||||||
@@ -605,11 +621,11 @@ async function applyDiscoveryAssignment(ip) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function saveOutputSettings() {
|
async function saveOutputSettings() {
|
||||||
if (!appState.snapshot) {
|
if (!appState.snapshot || appState.outputSaving) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ensureOutputDraft();
|
ensureOutputDraft();
|
||||||
const current = appState.snapshot.technical;
|
const current = snapshotTechnical(appState.snapshot);
|
||||||
const draft = appState.outputDraft;
|
const draft = appState.outputDraft;
|
||||||
|
|
||||||
const commands = [];
|
const commands = [];
|
||||||
@@ -638,6 +654,10 @@ async function saveOutputSettings() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
appState.outputSaving = true;
|
||||||
|
renderOutputControls();
|
||||||
|
|
||||||
|
try {
|
||||||
for (const command of commands) {
|
for (const command of commands) {
|
||||||
const response = await sendCommand(command.type, command.payload);
|
const response = await sendCommand(command.type, command.payload);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@@ -648,6 +668,10 @@ async function saveOutputSettings() {
|
|||||||
appState.outputDirty = false;
|
appState.outputDirty = false;
|
||||||
setFeedback("success", "Backend/output settings applied.");
|
setFeedback("success", "Backend/output settings applied.");
|
||||||
await loadState();
|
await loadState();
|
||||||
|
} finally {
|
||||||
|
appState.outputSaving = false;
|
||||||
|
renderOutputControls();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveNode(nodeId) {
|
async function saveNode(nodeId) {
|
||||||
@@ -773,6 +797,34 @@ function ensureOutputDraft() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function snapshotSystem(snapshot) {
|
||||||
|
return snapshot?.system ?? {
|
||||||
|
project_name: "Infinity Vis",
|
||||||
|
topology_label: "Technical surface waiting for snapshot",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function snapshotTechnical(snapshot) {
|
||||||
|
return snapshot?.technical ?? {
|
||||||
|
backend_mode: "preview_only",
|
||||||
|
output_enabled: false,
|
||||||
|
output_fps: 40,
|
||||||
|
live_status: "Waiting for technical state...",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function snapshotNodes(snapshot) {
|
||||||
|
return Array.isArray(snapshot?.nodes) ? snapshot.nodes : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function snapshotPanels(snapshot) {
|
||||||
|
return Array.isArray(snapshot?.panels) ? snapshot.panels : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function snapshotRecentEvents(snapshot) {
|
||||||
|
return Array.isArray(snapshot?.recent_events) ? snapshot.recent_events : [];
|
||||||
|
}
|
||||||
|
|
||||||
function ensureNodeDraft(nodeId) {
|
function ensureNodeDraft(nodeId) {
|
||||||
const existing = appState.nodeDrafts.get(nodeId);
|
const existing = appState.nodeDrafts.get(nodeId);
|
||||||
if (existing) {
|
if (existing) {
|
||||||
@@ -806,12 +858,12 @@ function assignedNodeForIp(ip) {
|
|||||||
if (!appState.snapshot || !ip) {
|
if (!appState.snapshot || !ip) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const match = appState.snapshot.nodes.find((node) => node.reserved_ip === ip);
|
const match = snapshotNodes(appState.snapshot).find((node) => node.reserved_ip === ip);
|
||||||
return match ? match.node_id : null;
|
return match ? match.node_id : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderNodeOptions(selectedNodeId) {
|
function renderNodeOptions(selectedNodeId) {
|
||||||
const nodes = appState.snapshot ? appState.snapshot.nodes : [];
|
const nodes = snapshotNodes(appState.snapshot);
|
||||||
return nodes
|
return nodes
|
||||||
.map((node) => {
|
.map((node) => {
|
||||||
const selected = node.node_id === selectedNodeId ? "selected" : "";
|
const selected = node.node_id === selectedNodeId ? "selected" : "";
|
||||||
|
|||||||
Reference in New Issue
Block a user