diff --git a/plugins.json b/plugins.json index 5b8f3de6..df1ae59e 100644 --- a/plugins.json +++ b/plugins.json @@ -101,7 +101,7 @@ "last_updated": "2026-08-13", "verified": true, "screenshot": "", - "latest_version": "1.16.0" + "latest_version": "1.16.1" }, { "id": "calendar", @@ -215,7 +215,7 @@ "last_updated": "2026-08-05", "verified": true, "screenshot": "", - "latest_version": "1.8.3" + "latest_version": "1.8.4" }, { "id": "football-scoreboard", @@ -439,7 +439,7 @@ "last_updated": "2026-08-03", "verified": true, "screenshot": "", - "latest_version": "1.12.11" + "latest_version": "1.12.12" }, { "id": "march-madness", @@ -487,7 +487,7 @@ "last_updated": "2026-08-05", "verified": true, "screenshot": "", - "latest_version": "2.5.4" + "latest_version": "2.5.5" }, { "id": "mqtt-notifications", @@ -760,7 +760,7 @@ "last_updated": "2026-08-13", "verified": true, "screenshot": "", - "latest_version": "2.12.0" + "latest_version": "2.12.1" }, { "id": "static-image", @@ -1048,7 +1048,7 @@ "downloads": 0, "verified": true, "screenshot": "", - "latest_version": "1.9.0", + "latest_version": "1.9.1", "last_updated": "2026-08-13" }, { @@ -1095,7 +1095,7 @@ "last_updated": "2026-08-13", "verified": true, "screenshot": "", - "latest_version": "1.9.0" + "latest_version": "1.9.1" }, { "id": "jellyfin-now-playing", diff --git a/plugins/afl-scoreboard/config_schema.json b/plugins/afl-scoreboard/config_schema.json index a6beea3e..bd7cbb7f 100644 --- a/plugins/afl-scoreboard/config_schema.json +++ b/plugins/afl-scoreboard/config_schema.json @@ -566,7 +566,7 @@ "mode_durations": { "type": "object", "title": "Mode-Level Durations", - "description": "Control total duration for each mode type. If not set, uses dynamic calculation (total_games \u00d7 per_game_duration).", + "description": "Control total duration for each mode type. If not set, uses dynamic calculation (total_games × per_game_duration).", "properties": { "recent_mode_duration": { "type": [ @@ -1330,6 +1330,9 @@ "background_service", "customization", "schedule_lookback_days", - "schedule_lookahead_days" + "schedule_lookahead_days", + "scroll_card", + "no_data_interval_seconds", + "live_idle_max_interval_seconds" ] } diff --git a/plugins/afl-scoreboard/manifest.json b/plugins/afl-scoreboard/manifest.json index 13d1d7d4..a4669c76 100644 --- a/plugins/afl-scoreboard/manifest.json +++ b/plugins/afl-scoreboard/manifest.json @@ -1,7 +1,7 @@ { "id": "afl-scoreboard", "name": "AFL Scoreboard", - "version": "1.9.0", + "version": "1.9.1", "author": "ChuckBuilds", "description": "Live, recent, and upcoming AFL (Australian Football League) games with real-time scores and game status.", "category": "sports", @@ -18,6 +18,12 @@ "afl_upcoming" ], "versions": [ + { + "version": "1.9.1", + "released": "2026-08-19", + "ledmatrix_min_version": "2.0.0", + "notes": "Make scroll_card and the two idle-poll intervals reachable in the web UI. The config form renders only the properties named in x-propertyOrder, so a setting the schema declares but that list omits was never drawn -- no field, no error, no hint it existed, and the only way to set it was to hand-edit config.json on the device. The values already validated on save and the plugin already read them; only the form was missing them." + }, { "version": "1.9.0", "released": "2026-08-19", diff --git a/plugins/basketball-scoreboard/config_schema.json b/plugins/basketball-scoreboard/config_schema.json index 2e389166..ba4afe4f 100644 --- a/plugins/basketball-scoreboard/config_schema.json +++ b/plugins/basketball-scoreboard/config_schema.json @@ -2558,6 +2558,10 @@ "ncaaw", "customization", "schedule_lookback_days", - "schedule_lookahead_days" + "schedule_lookahead_days", + "scroll_card", + "background_service", + "no_data_interval_seconds", + "live_idle_max_interval_seconds" ] } diff --git a/plugins/basketball-scoreboard/manifest.json b/plugins/basketball-scoreboard/manifest.json index 5e094852..c64e9694 100644 --- a/plugins/basketball-scoreboard/manifest.json +++ b/plugins/basketball-scoreboard/manifest.json @@ -1,7 +1,7 @@ { "id": "basketball-scoreboard", "name": "Basketball Scoreboard", - "version": "1.16.0", + "version": "1.16.1", "description": "Live, recent, and upcoming basketball games across NBA, NCAA Men's, NCAA Women's, and WNBA with real-time scores, schedules, and March Madness tournament support", "author": "ChuckBuilds", "category": "sports", @@ -18,6 +18,12 @@ "branch": "main", "plugin_path": "plugins/basketball-scoreboard", "versions": [ + { + "version": "1.16.1", + "released": "2026-08-19", + "ledmatrix_min_version": "2.0.0", + "notes": "Make scroll_card, background_service and the two idle-poll intervals reachable in the web UI. The config form renders only the properties named in x-propertyOrder, so a setting the schema declares but that list omits was never drawn -- no field, no error, no hint it existed, and the only way to set it was to hand-edit config.json on the device. The values already validated on save and the plugin already read them; only the form was missing them." + }, { "version": "1.16.0", "released": "2026-08-19", diff --git a/plugins/f1-scoreboard/config_schema.json b/plugins/f1-scoreboard/config_schema.json index 0830dc3f..3dd2bbc5 100644 --- a/plugins/f1-scoreboard/config_schema.json +++ b/plugins/f1-scoreboard/config_schema.json @@ -73,7 +73,11 @@ "description": "Always include favorite driver even if outside top N" } }, - "x-propertyOrder": ["enabled", "top_n", "always_show_favorite"], + "x-propertyOrder": [ + "enabled", + "top_n", + "always_show_favorite" + ], "additionalProperties": false }, "constructor_standings": { @@ -111,7 +115,12 @@ "description": "Show each constructor's two drivers and their individual point contributions (e.g. NOR 189 | PIA 145)" } }, - "x-propertyOrder": ["enabled", "top_n", "always_show_favorite", "show_driver_split"], + "x-propertyOrder": [ + "enabled", + "top_n", + "always_show_favorite", + "show_driver_split" + ], "additionalProperties": false }, "recent_races": { @@ -204,7 +213,19 @@ "description": "Number of drivers to show on the race gap bar chart (3-10)" } }, - "x-propertyOrder": ["enabled", "number_of_races", "top_finishers", "always_show_favorite", "show_position_delta", "show_dnf_status", "show_points_haul", "points_haul_drivers", "show_winners_summary", "show_gap_chart", "gap_chart_drivers"], + "x-propertyOrder": [ + "enabled", + "number_of_races", + "top_finishers", + "always_show_favorite", + "show_position_delta", + "show_dnf_status", + "show_points_haul", + "points_haul_drivers", + "show_winners_summary", + "show_gap_chart", + "gap_chart_drivers" + ], "additionalProperties": false }, "upcoming": { @@ -240,7 +261,12 @@ "description": "Show a circuit facts card after the upcoming race card: laps, distance, and lap record" } }, - "x-propertyOrder": ["enabled", "show_session_times", "countdown_enabled", "show_circuit_info"], + "x-propertyOrder": [ + "enabled", + "show_session_times", + "countdown_enabled", + "show_circuit_info" + ], "additionalProperties": false }, "qualifying": { @@ -290,7 +316,14 @@ "description": "Show a summary card at the start of qualifying with each team's intra-team battle (which driver outqualified their teammate and by how many positions)" } }, - "x-propertyOrder": ["enabled", "show_q3", "show_q2", "show_q1", "show_gaps", "show_team_duel"], + "x-propertyOrder": [ + "enabled", + "show_q3", + "show_q2", + "show_q1", + "show_gaps", + "show_team_duel" + ], "additionalProperties": false }, "practice": { @@ -310,9 +343,17 @@ "x-widget": "checkbox-group", "items": { "type": "string", - "enum": ["FP1", "FP2", "FP3"] + "enum": [ + "FP1", + "FP2", + "FP3" + ] }, - "default": ["FP1", "FP2", "FP3"], + "default": [ + "FP1", + "FP2", + "FP3" + ], "description": "Which free practice sessions to display" }, "top_n": { @@ -325,7 +366,11 @@ "description": "Number of top drivers to show per practice session" } }, - "x-propertyOrder": ["enabled", "sessions_to_show", "top_n"], + "x-propertyOrder": [ + "enabled", + "sessions_to_show", + "top_n" + ], "additionalProperties": false }, "sprint": { @@ -349,7 +394,10 @@ "description": "Number of top finishers to display" } }, - "x-propertyOrder": ["enabled", "top_finishers"], + "x-propertyOrder": [ + "enabled", + "top_finishers" + ], "additionalProperties": false }, "calendar": { @@ -394,7 +442,13 @@ "description": "Maximum number of upcoming race weekends to show" } }, - "x-propertyOrder": ["enabled", "show_practice", "show_qualifying", "show_sprint", "max_events"], + "x-propertyOrder": [ + "enabled", + "show_practice", + "show_qualifying", + "show_sprint", + "max_events" + ], "additionalProperties": false }, "vegas": { @@ -408,13 +462,31 @@ "x-widget": "checkbox-group", "items": { "type": "string", - "enum": ["leaders", "battles", "spotlight", "upcoming", "last_race", "driver_standings", "constructor_standings", "recent_races", "qualifying", "practice", "sprint", "calendar"] + "enum": [ + "leaders", + "battles", + "spotlight", + "upcoming", + "last_race", + "driver_standings", + "constructor_standings", + "recent_races", + "qualifying", + "practice", + "sprint", + "calendar" + ] }, - "default": ["upcoming", "last_race"], + "default": [ + "upcoming", + "last_race" + ], "description": "Which sections join the marquee. The order you list them in does not matter — they are always shown in the fixed order leaders, battles, spotlight, upcoming, last_race, driver_standings, constructor_standings, recent_races, qualifying, practice, sprint, calendar. Default is the next race and the last race's results. Adding more makes F1 hold the marquee for longer — every section at once measured 114 cards, close to six minutes of unbroken F1 on a 512px panel. 'last_race' is the most recent race only; 'recent_races' is all of them. Leave empty to keep F1 out of the marquee entirely." } }, - "x-propertyOrder": ["sections"], + "x-propertyOrder": [ + "sections" + ], "additionalProperties": false }, "dynamic_duration": { @@ -439,7 +511,10 @@ "description": "Maximum seconds before forcing rotation even if scroll is incomplete" } }, - "x-propertyOrder": ["enabled", "max_duration_seconds"], + "x-propertyOrder": [ + "enabled", + "max_duration_seconds" + ], "additionalProperties": false }, "scroll": { @@ -498,14 +573,25 @@ "x-advanced": true, "type": "array", "title": "Color (RGB)", - "items": { "type": "integer", "minimum": 0, "maximum": 255 }, + "items": { + "type": "integer", + "minimum": 0, + "maximum": 255 + }, "minItems": 3, "maxItems": 3, - "default": [180, 0, 255], + "default": [ + 180, + 0, + 255 + ], "description": "RGB color of the fastest lap dot (default: purple [180, 0, 255])" } }, - "x-propertyOrder": ["enabled", "color"], + "x-propertyOrder": [ + "enabled", + "color" + ], "additionalProperties": false }, "gap_bar": { @@ -521,7 +607,9 @@ "description": "Show the points gap bar in driver and constructor standings" } }, - "x-propertyOrder": ["enabled"], + "x-propertyOrder": [ + "enabled" + ], "additionalProperties": false }, "standings_header": { @@ -544,7 +632,10 @@ "description": "Show current round number and season progress bar on the header card" } }, - "x-propertyOrder": ["enabled", "show_round"], + "x-propertyOrder": [ + "enabled", + "show_round" + ], "additionalProperties": false }, "circuit_map": { @@ -560,7 +651,9 @@ "description": "Show circuit layout diagram on the upcoming race card" } }, - "x-propertyOrder": ["enabled"], + "x-propertyOrder": [ + "enabled" + ], "additionalProperties": false }, "championship_leaders": { @@ -576,7 +669,9 @@ "description": "Show the championship leaders intro card in the scroll sequence" } }, - "x-propertyOrder": ["enabled"], + "x-propertyOrder": [ + "enabled" + ], "additionalProperties": false }, "championship_battle": { @@ -592,7 +687,9 @@ "description": "Show the driver championship battle card in the scroll sequence" } }, - "x-propertyOrder": ["enabled"], + "x-propertyOrder": [ + "enabled" + ], "additionalProperties": false }, "constructor_battle": { @@ -608,7 +705,9 @@ "description": "Show the constructor championship battle card in the scroll sequence" } }, - "x-propertyOrder": ["enabled"], + "x-propertyOrder": [ + "enabled" + ], "additionalProperties": false }, "driver_form": { @@ -624,7 +723,9 @@ "description": "Show the driver form guide card at the start of the driver standings section" } }, - "x-propertyOrder": ["enabled"], + "x-propertyOrder": [ + "enabled" + ], "additionalProperties": false }, "last_race_points": { @@ -640,11 +741,23 @@ "description": "Show last-race points (+N) in the stat zone of each driver and constructor standings card" } }, - "x-propertyOrder": ["enabled"], + "x-propertyOrder": [ + "enabled" + ], "additionalProperties": false } }, - "x-propertyOrder": ["fastest_lap_dot", "gap_bar", "standings_header", "circuit_map", "championship_leaders", "championship_battle", "constructor_battle", "driver_form", "last_race_points"], + "x-propertyOrder": [ + "fastest_lap_dot", + "gap_bar", + "standings_header", + "circuit_map", + "championship_leaders", + "championship_battle", + "constructor_battle", + "driver_form", + "last_race_points" + ], "additionalProperties": false }, "customization": { @@ -667,7 +780,18 @@ "x-advanced": true, "type": "string", "title": "Font", - "enum": ["", "4x6.bdf", "5x8.bdf", "6x10.bdf", "7x13.bdf", "9x15.bdf", "10x20.bdf", "PressStart2P-Regular.ttf", "4x6-font.ttf", "5by7.regular.ttf"], + "enum": [ + "", + "4x6.bdf", + "5x8.bdf", + "6x10.bdf", + "7x13.bdf", + "9x15.bdf", + "10x20.bdf", + "PressStart2P-Regular.ttf", + "4x6-font.ttf", + "5by7.regular.ttf" + ], "default": "" }, "font_size": { @@ -679,7 +803,10 @@ "default": 8 } }, - "x-propertyOrder": ["font", "font_size"], + "x-propertyOrder": [ + "font", + "font_size" + ], "additionalProperties": false }, "position_text": { @@ -691,7 +818,18 @@ "x-advanced": true, "type": "string", "title": "Font", - "enum": ["", "4x6.bdf", "5x8.bdf", "6x10.bdf", "7x13.bdf", "9x15.bdf", "10x20.bdf", "PressStart2P-Regular.ttf", "4x6-font.ttf", "5by7.regular.ttf"], + "enum": [ + "", + "4x6.bdf", + "5x8.bdf", + "6x10.bdf", + "7x13.bdf", + "9x15.bdf", + "10x20.bdf", + "PressStart2P-Regular.ttf", + "4x6-font.ttf", + "5by7.regular.ttf" + ], "default": "" }, "font_size": { @@ -703,7 +841,10 @@ "default": 8 } }, - "x-propertyOrder": ["font", "font_size"], + "x-propertyOrder": [ + "font", + "font_size" + ], "additionalProperties": false }, "detail_text": { @@ -715,7 +856,18 @@ "x-advanced": true, "type": "string", "title": "Font", - "enum": ["", "4x6.bdf", "5x8.bdf", "6x10.bdf", "7x13.bdf", "9x15.bdf", "10x20.bdf", "PressStart2P-Regular.ttf", "4x6-font.ttf", "5by7.regular.ttf"], + "enum": [ + "", + "4x6.bdf", + "5x8.bdf", + "6x10.bdf", + "7x13.bdf", + "9x15.bdf", + "10x20.bdf", + "PressStart2P-Regular.ttf", + "4x6-font.ttf", + "5by7.regular.ttf" + ], "default": "" }, "font_size": { @@ -727,7 +879,10 @@ "default": 6 } }, - "x-propertyOrder": ["font", "font_size"], + "x-propertyOrder": [ + "font", + "font_size" + ], "additionalProperties": false }, "small_text": { @@ -739,7 +894,18 @@ "x-advanced": true, "type": "string", "title": "Font", - "enum": ["", "4x6.bdf", "5x8.bdf", "6x10.bdf", "7x13.bdf", "9x15.bdf", "10x20.bdf", "PressStart2P-Regular.ttf", "4x6-font.ttf", "5by7.regular.ttf"], + "enum": [ + "", + "4x6.bdf", + "5x8.bdf", + "6x10.bdf", + "7x13.bdf", + "9x15.bdf", + "10x20.bdf", + "PressStart2P-Regular.ttf", + "4x6-font.ttf", + "5by7.regular.ttf" + ], "default": "" }, "font_size": { @@ -751,15 +917,46 @@ "default": 6 } }, - "x-propertyOrder": ["font", "font_size"], + "x-propertyOrder": [ + "font", + "font_size" + ], "additionalProperties": false } }, - "x-propertyOrder": ["header_text", "position_text", "detail_text", "small_text"], + "x-propertyOrder": [ + "header_text", + "position_text", + "detail_text", + "small_text", + "auto_scale" + ], "additionalProperties": false } }, - "x-propertyOrder": ["enabled", "display_duration", "update_interval", "favorite_team", "favorite_driver", "timezone", "driver_standings", "constructor_standings", "recent_races", "upcoming", "qualifying", "practice", "sprint", "calendar", "vegas", "dynamic_duration", "scroll", "visual", "customization"], + "x-propertyOrder": [ + "enabled", + "display_duration", + "update_interval", + "favorite_team", + "favorite_driver", + "timezone", + "driver_standings", + "constructor_standings", + "recent_races", + "upcoming", + "qualifying", + "practice", + "sprint", + "calendar", + "vegas", + "dynamic_duration", + "scroll", + "visual", + "customization" + ], "additionalProperties": false, - "required": ["enabled"] + "required": [ + "enabled" + ] } diff --git a/plugins/f1-scoreboard/manifest.json b/plugins/f1-scoreboard/manifest.json index 8992e247..57a9db58 100644 --- a/plugins/f1-scoreboard/manifest.json +++ b/plugins/f1-scoreboard/manifest.json @@ -1,7 +1,7 @@ { "id": "f1-scoreboard", "name": "F1 Scoreboard", - "version": "1.8.3", + "version": "1.8.4", "author": "ChuckBuilds", "class_name": "F1ScoreboardPlugin", "entry_point": "manager.py", @@ -29,6 +29,12 @@ "f1_calendar" ], "versions": [ + { + "version": "1.8.4", + "released": "2026-08-19", + "ledmatrix_min_version": "2.0.0", + "notes": "Make customization.auto_scale reachable in the web UI. The config form renders only the properties named in x-propertyOrder, so a setting the schema declares but that list omits was never drawn -- no field, no error, no hint it existed, and the only way to set it was to hand-edit config.json on the device. The values already validated on save and the plugin already read them; only the form was missing them." + }, { "version": "1.8.3", "released": "2026-08-14", diff --git a/plugins/ledmatrix-flights/config_schema.json b/plugins/ledmatrix-flights/config_schema.json index 0b9b0a13..972209e3 100644 --- a/plugins/ledmatrix-flights/config_schema.json +++ b/plugins/ledmatrix-flights/config_schema.json @@ -388,7 +388,10 @@ "type": "string", "title": "Altimeter Unit", "description": "Altimeter setting display: 'inhg' shows US style (A30.01), 'hpa' shows international style (Q1016).", - "enum": ["inhg", "hpa"], + "enum": [ + "inhg", + "hpa" + ], "default": "inhg" }, "temp_unit": { @@ -396,7 +399,10 @@ "type": "string", "title": "Temperature Unit", "description": "Temperature/dewpoint unit on the decoded card.", - "enum": ["c", "f"], + "enum": [ + "c", + "f" + ], "default": "c" }, "wind_unit": { @@ -404,7 +410,12 @@ "type": "string", "title": "Wind Speed Unit", "description": "Wind speed unit. Aviation standard is knots (kt).", - "enum": ["kt", "mph", "kmh", "ms"], + "enum": [ + "kt", + "mph", + "kmh", + "ms" + ], "default": "kt" }, "visibility_unit": { @@ -412,7 +423,11 @@ "type": "string", "title": "Visibility Unit", "description": "Visibility unit: statute miles (US), metres or kilometres (international).", - "enum": ["sm", "m", "km"], + "enum": [ + "sm", + "m", + "km" + ], "default": "sm" } }, @@ -690,7 +705,7 @@ "x-advanced": true, "type": "boolean", "title": "Show Airline Logos", - "description": "Show airline logos in area mode (8\u00d78 pixel sprites next to callsigns). Flight detail layouts always show logos in the logo zone when available (52 airlines included as PNG assets).", + "description": "Show airline logos in area mode (8×8 pixel sprites next to callsigns). Flight detail layouts always show logos in the logo zone when available (52 airlines included as PNG assets).", "default": true }, "scroll_speed": { @@ -730,17 +745,17 @@ "flightaware": { "type": "object", "title": "FlightAware AeroAPI (Paid - Optional)", - "description": "OPTIONAL PAID SERVICE \u2014 skip this section entirely unless you have a FlightAware AeroAPI subscription. Enabling this will make API calls that are billed at $0.005 each. Free route data via FR24 enrichment (above) works well for most users without any cost.", + "description": "OPTIONAL PAID SERVICE — skip this section entirely unless you have a FlightAware AeroAPI subscription. Enabling this will make API calls that are billed at $0.005 each. Free route data via FR24 enrichment (above) works well for most users without any cost.", "properties": { "api_key": { "type": "string", - "description": "Your FlightAware AeroAPI key. Leave blank if you are not using FlightAware \u2014 the plugin works fully without this.", + "description": "Your FlightAware AeroAPI key. Leave blank if you are not using FlightAware — the plugin works fully without this.", "x-secret": true, "default": "" }, "enabled": { "type": "boolean", - "description": "Enable paid FlightAware API calls. Must be true AND api_key must be set for FlightAware to be used. Disabled by default \u2014 do not enable unless you have a paid subscription.", + "description": "Enable paid FlightAware API calls. Must be true AND api_key must be set for FlightAware to be used. Disabled by default — do not enable unless you have a paid subscription.", "default": false }, "max_api_calls_per_hour": { @@ -966,6 +981,14 @@ "offline_database_update_interval_days", "opensky_username", "opensky_password", - "flightaware" + "flightaware", + "flightaware_api_key", + "flight_plan_enabled", + "max_api_calls_per_hour", + "daily_api_budget", + "flight_plan_cache_ttl_hours", + "min_callsign_length", + "airline_callsign_prefixes", + "background_service" ] } diff --git a/plugins/ledmatrix-flights/manifest.json b/plugins/ledmatrix-flights/manifest.json index 10a81e0d..77e01fba 100644 --- a/plugins/ledmatrix-flights/manifest.json +++ b/plugins/ledmatrix-flights/manifest.json @@ -1,7 +1,7 @@ { "id": "ledmatrix-flights", "name": "Flight Tracker", - "version": "1.12.11", + "version": "1.12.12", "description": "Real-time aircraft tracking with ADS-B/FlightRadar24/OpenSky/adsb.fi/adsb.lol data, map backgrounds, area mode, flight tracking, anchor airport, flight records, and optional airport weather (METAR/TAF/PIREP/SIGMET via the free NOAA Aviation Weather Center API)", "author": "ChuckBuilds", "entry_point": "manager.py", @@ -37,6 +37,12 @@ "min_ledmatrix_version": "2.0.0", "max_ledmatrix_version": "3.0.0", "versions": [ + { + "version": "1.12.12", + "released": "2026-08-19", + "ledmatrix_min_version": "2.0.0", + "notes": "Make the FlightAware API key, the flight-plan settings and background_service reachable in the web UI. The config form renders only the properties named in x-propertyOrder, so a setting the schema declares but that list omits was never drawn -- no field, no error, no hint it existed, and the only way to set it was to hand-edit config.json on the device. The values already validated on save and the plugin already read them; only the form was missing them." + }, { "version": "1.12.11", "released": "2026-08-18", diff --git a/plugins/masters-tournament/config_schema.json b/plugins/masters-tournament/config_schema.json index 0c63ac6b..64343d02 100644 --- a/plugins/masters-tournament/config_schema.json +++ b/plugins/masters-tournament/config_schema.json @@ -178,7 +178,11 @@ "minimum": 1, "maximum": 18 }, - "default": [12, 13, 16], + "default": [ + 12, + 13, + 16 + ], "description": "Featured holes to highlight (Amen Corner, par 3s)" } } @@ -451,7 +455,11 @@ "color_scheme": { "x-advanced": true, "type": "string", - "enum": ["classic", "bright", "subdued"], + "enum": [ + "classic", + "bright", + "subdued" + ], "default": "classic", "description": "Masters color palette variant" } @@ -459,7 +467,9 @@ } }, "additionalProperties": false, - "required": ["enabled"], + "required": [ + "enabled" + ], "x-propertyOrder": [ "enabled", "display_duration", @@ -469,6 +479,10 @@ "favorite_players", "display_modes", "notifications", - "branding" + "branding", + "player_card_duration", + "hole_display_duration", + "page_display_duration", + "scroll_card_width" ] } diff --git a/plugins/masters-tournament/manifest.json b/plugins/masters-tournament/manifest.json index 60f85be8..1d655261 100644 --- a/plugins/masters-tournament/manifest.json +++ b/plugins/masters-tournament/manifest.json @@ -1,7 +1,7 @@ { "id": "masters-tournament", "name": "Masters Tournament", - "version": "2.5.4", + "version": "2.5.5", "description": "Broadcast-quality Masters Tournament display with real ESPN player headshots, accurate Augusta National hole layouts, fun facts, past champions, live leaderboards, and pixel-perfect LED matrix rendering", "author": "ChuckBuilds", "class_name": "MastersTournamentPlugin", @@ -43,6 +43,12 @@ "height": 64 }, "versions": [ + { + "version": "2.5.5", + "released": "2026-08-19", + "ledmatrix_min_version": "2.0.0", + "notes": "Make the player-card, hole, page and scroll-card sizing settings reachable in the web UI. The config form renders only the properties named in x-propertyOrder, so a setting the schema declares but that list omits was never drawn -- no field, no error, no hint it existed, and the only way to set it was to hand-edit config.json on the device. The values already validated on save and the plugin already read them; only the form was missing them." + }, { "version": "2.5.4", "released": "2026-08-05", diff --git a/plugins/nrl-scoreboard/config_schema.json b/plugins/nrl-scoreboard/config_schema.json index f67bcd81..9b82e2e7 100644 --- a/plugins/nrl-scoreboard/config_schema.json +++ b/plugins/nrl-scoreboard/config_schema.json @@ -562,7 +562,7 @@ "mode_durations": { "type": "object", "title": "Mode-Level Durations", - "description": "Control total duration for each mode type. If not set, uses dynamic calculation (total_games \u00d7 per_game_duration).", + "description": "Control total duration for each mode type. If not set, uses dynamic calculation (total_games × per_game_duration).", "properties": { "recent_mode_duration": { "type": [ @@ -1299,6 +1299,9 @@ "background_service", "customization", "schedule_lookback_days", - "schedule_lookahead_days" + "schedule_lookahead_days", + "scroll_card", + "no_data_interval_seconds", + "live_idle_max_interval_seconds" ] } diff --git a/plugins/nrl-scoreboard/manifest.json b/plugins/nrl-scoreboard/manifest.json index 566d751d..43f831ac 100644 --- a/plugins/nrl-scoreboard/manifest.json +++ b/plugins/nrl-scoreboard/manifest.json @@ -1,7 +1,7 @@ { "id": "nrl-scoreboard", "name": "NRL Scoreboard", - "version": "1.9.0", + "version": "1.9.1", "author": "ChuckBuilds", "description": "Live, recent, and upcoming NRL (National Rugby League) games with real-time scores and game status.", "category": "sports", @@ -18,6 +18,12 @@ "nrl_upcoming" ], "versions": [ + { + "version": "1.9.1", + "released": "2026-08-19", + "ledmatrix_min_version": "2.0.0", + "notes": "Make scroll_card and the two idle-poll intervals reachable in the web UI. The config form renders only the properties named in x-propertyOrder, so a setting the schema declares but that list omits was never drawn -- no field, no error, no hint it existed, and the only way to set it was to hand-edit config.json on the device. The values already validated on save and the plugin already read them; only the form was missing them." + }, { "version": "1.9.0", "released": "2026-08-19", diff --git a/plugins/soccer-scoreboard/config_schema.json b/plugins/soccer-scoreboard/config_schema.json index 907194ea..d810c744 100644 --- a/plugins/soccer-scoreboard/config_schema.json +++ b/plugins/soccer-scoreboard/config_schema.json @@ -641,7 +641,7 @@ "mode_durations": { "type": "object", "title": "Mode-Level Durations", - "description": "Control total duration for each mode type. If not set, uses dynamic calculation (total_games \u00d7 per_game_duration).", + "description": "Control total duration for each mode type. If not set, uses dynamic calculation (total_games × per_game_duration).", "properties": { "recent_mode_duration": { "type": [ @@ -1050,7 +1050,7 @@ "mode_durations": { "type": "object", "title": "Mode-Level Durations", - "description": "Control total duration for each mode type. If not set, uses dynamic calculation (total_games \u00d7 per_game_duration).", + "description": "Control total duration for each mode type. If not set, uses dynamic calculation (total_games × per_game_duration).", "properties": { "recent_mode_duration": { "type": [ @@ -1459,7 +1459,7 @@ "mode_durations": { "type": "object", "title": "Mode-Level Durations", - "description": "Control total duration for each mode type. If not set, uses dynamic calculation (total_games \u00d7 per_game_duration).", + "description": "Control total duration for each mode type. If not set, uses dynamic calculation (total_games × per_game_duration).", "properties": { "recent_mode_duration": { "type": [ @@ -1868,7 +1868,7 @@ "mode_durations": { "type": "object", "title": "Mode-Level Durations", - "description": "Control total duration for each mode type. If not set, uses dynamic calculation (total_games \u00d7 per_game_duration).", + "description": "Control total duration for each mode type. If not set, uses dynamic calculation (total_games × per_game_duration).", "properties": { "recent_mode_duration": { "type": [ @@ -2277,7 +2277,7 @@ "mode_durations": { "type": "object", "title": "Mode-Level Durations", - "description": "Control total duration for each mode type. If not set, uses dynamic calculation (total_games \u00d7 per_game_duration).", + "description": "Control total duration for each mode type. If not set, uses dynamic calculation (total_games × per_game_duration).", "properties": { "recent_mode_duration": { "type": [ @@ -2686,7 +2686,7 @@ "mode_durations": { "type": "object", "title": "Mode-Level Durations", - "description": "Control total duration for each mode type. If not set, uses dynamic calculation (total_games \u00d7 per_game_duration).", + "description": "Control total duration for each mode type. If not set, uses dynamic calculation (total_games × per_game_duration).", "properties": { "recent_mode_duration": { "type": [ @@ -3095,7 +3095,7 @@ "mode_durations": { "type": "object", "title": "Mode-Level Durations", - "description": "Control total duration for each mode type. If not set, uses dynamic calculation (total_games \u00d7 per_game_duration).", + "description": "Control total duration for each mode type. If not set, uses dynamic calculation (total_games × per_game_duration).", "properties": { "recent_mode_duration": { "type": [ @@ -3504,7 +3504,7 @@ "mode_durations": { "type": "object", "title": "Mode-Level Durations", - "description": "Control total duration for each mode type. If not set, uses dynamic calculation (total_games \u00d7 per_game_duration).", + "description": "Control total duration for each mode type. If not set, uses dynamic calculation (total_games × per_game_duration).", "properties": { "recent_mode_duration": { "type": [ @@ -3913,7 +3913,7 @@ "mode_durations": { "type": "object", "title": "Mode-Level Durations", - "description": "Control total duration for each mode type. If not set, uses dynamic calculation (total_games \u00d7 per_game_duration).", + "description": "Control total duration for each mode type. If not set, uses dynamic calculation (total_games × per_game_duration).", "properties": { "recent_mode_duration": { "type": [ @@ -4322,7 +4322,7 @@ "mode_durations": { "type": "object", "title": "Mode-Level Durations", - "description": "Control total duration for each mode type. If not set, uses dynamic calculation (total_games \u00d7 per_game_duration).", + "description": "Control total duration for each mode type. If not set, uses dynamic calculation (total_games × per_game_duration).", "properties": { "recent_mode_duration": { "type": [ @@ -4370,7 +4370,7 @@ "custom_leagues": { "type": "array", "title": "Add More Leagues", - "description": "Add any soccer league available on ESPN. Click 'Add Item', then fill in BOTH a name and a league code. Common codes: eng.2 (English Championship), eng.3 (League One), eng.fa (FA Cup), eng.league_cup (EFL Cup), mex.1 (Liga MX), arg.1 (Argentina), bra.1 (Brazil), ned.1 (Eredivisie), sco.1 (Scottish Premiership), tur.1 (Turkish S\u00fcper Lig), bel.1 (Belgian Pro League)", + "description": "Add any soccer league available on ESPN. Click 'Add Item', then fill in BOTH a name and a league code. Common codes: eng.2 (English Championship), eng.3 (League One), eng.fa (FA Cup), eng.league_cup (EFL Cup), mex.1 (Liga MX), arg.1 (Argentina), bra.1 (Brazil), ned.1 (Eredivisie), sco.1 (Scottish Premiership), tur.1 (Turkish Süper Lig), bel.1 (Belgian Pro League)", "x-widget": "array-table", "x-columns": [ "name", @@ -5404,6 +5404,9 @@ "leagues", "customization", "schedule_lookback_days", - "schedule_lookahead_days" + "schedule_lookahead_days", + "scroll_card", + "no_data_interval_seconds", + "live_idle_max_interval_seconds" ] } diff --git a/plugins/soccer-scoreboard/manifest.json b/plugins/soccer-scoreboard/manifest.json index 6e512f25..5c63b691 100644 --- a/plugins/soccer-scoreboard/manifest.json +++ b/plugins/soccer-scoreboard/manifest.json @@ -1,7 +1,7 @@ { "id": "soccer-scoreboard", "name": "Soccer Scoreboard", - "version": "2.12.0", + "version": "2.12.1", "author": "ChuckBuilds", "description": "Live, recent, and upcoming soccer games across multiple leagues including Premier League, La Liga, Bundesliga, Serie A, Ligue 1, MLS, Liga Portugal, Champions League, Europa League, and FIFA World Cup", "category": "sports", @@ -26,6 +26,12 @@ "soccer_upcoming" ], "versions": [ + { + "version": "2.12.1", + "released": "2026-08-19", + "ledmatrix_min_version": "2.0.0", + "notes": "Make scroll_card and the two idle-poll intervals reachable in the web UI. The config form renders only the properties named in x-propertyOrder, so a setting the schema declares but that list omits was never drawn -- no field, no error, no hint it existed, and the only way to set it was to hand-edit config.json on the device. The values already validated on save and the plugin already read them; only the form was missing them." + }, { "version": "2.12.0", "released": "2026-08-19", @@ -149,7 +155,7 @@ { "released": "2026-07-29", "version": "2.5.0", - "notes": "Corrected every team code in TEAMS.md against ESPN's live data \u2014 Manchester United is MAN (not MUN), Manchester City MNC (not MCI), Real Madrid RMA, and Ligue 1 had eight wrong codes. The plugin now also says why a league is empty: an unrecognised favorite team logs a warning naming the closest match, while a correct code in a league with no fixtures yet logs the date the season starts.", + "notes": "Corrected every team code in TEAMS.md against ESPN's live data — Manchester United is MAN (not MUN), Manchester City MNC (not MCI), Real Madrid RMA, and Ligue 1 had eight wrong codes. The plugin now also says why a league is empty: an unrecognised favorite team logs a warning naming the closest match, while a correct code in a league with no fixtures yet logs the date the season starts.", "ledmatrix_min": "2.0.0" }, { @@ -173,7 +179,7 @@ { "released": "2026-07-02", "version": "2.2.0", - "notes": "Add exclude_teams (hide specific teams from live rotation and recent/final scores \u2014 spoiler protection) and filtering.favorite_live_boost (tune how much more often your favorite's live game appears in rotation vs other live games) per league, including custom leagues.", + "notes": "Add exclude_teams (hide specific teams from live rotation and recent/final scores — spoiler protection) and filtering.favorite_live_boost (tune how much more often your favorite's live game appears in rotation vs other live games) per league, including custom leagues.", "ledmatrix_min": "2.0.0" }, { diff --git a/scripts/test_property_order_coverage.py b/scripts/test_property_order_coverage.py new file mode 100644 index 00000000..59626f00 --- /dev/null +++ b/scripts/test_property_order_coverage.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python3 +"""A setting missing from x-propertyOrder cannot be configured at all. + +The web UI's config form iterates ``x-propertyOrder`` and nothing else: + + {% set property_order = schema['x-propertyOrder'] + if 'x-propertyOrder' in schema + else schema.properties.keys()|list %} + {% for key in property_order %} + {% if key in schema.properties %} + +A property declared in the schema but absent from that list is therefore +never rendered -- no field, no error, no hint that the setting exists. The +value still validates on save and the plugin still reads it, so the only way +to set one was to hand-edit config.json on the device. + +Twenty-six settings across seven plugins were in that state, including +ledmatrix-flights' ``flightaware_api_key`` -- marked ``x-secret: true`` for +masking, yet impossible to enter -- and the two idle-poll intervals whose +plumbing had just been fixed so they would finally take effect. Twenty-two of +the twenty-six carried ``x-advanced: true``: nobody flags a field "advanced" +meaning "invisible", so these were omissions, not deliberate hiding. There is +no supported way to hide a property, and no schema in the repo attempts one. + +Note the asymmetry that hid this: the client-side renderer in app-shell.js +appends unlisted properties instead of dropping them, so the same schema can +look fine there and be unreachable in the server-rendered form. + +Run: /bin/python scripts/test_property_order_coverage.py +""" + +import json +import sys +from pathlib import Path + +REPO = Path(__file__).resolve().parent.parent +failures = [] + + +def check(label, ok): + print((" PASS " if ok else " FAIL ") + label) + if not ok: + failures.append(label) + + +def unlisted(node, path=()): + """Every (location, property) a config form would silently skip.""" + out = [] + if not isinstance(node, dict): + return out + props = node.get("properties") + if isinstance(props, dict): + order = node.get("x-propertyOrder") + if isinstance(order, list): + for key in props: + if key not in order: + out.append((".".join(path) or "(root)", key)) + for key, value in props.items(): + out += unlisted(value, path + (key,)) + items = node.get("items") + if isinstance(items, dict): + out += unlisted(items, path + ("[]",)) + return out + + +def main(): + print("every declared setting must be reachable in the config form") + offenders = [] + schemas = sorted((REPO / "plugins").glob("*/config_schema.json")) + for path in schemas: + plugin = path.parent.name + missing = unlisted(json.load(path.open(encoding="utf-8"))) + if missing: + offenders.append((plugin, missing)) + for plugin, missing in offenders: + for loc, key in missing: + print(f" {plugin}: {loc}.{key}") + check(f"{len(schemas)} schemas, none hiding a declared setting " + f"({sum(len(m) for _, m in offenders)} hidden)", not offenders) + + print("\nthe form really does drop what the order omits") + # Not a claim about the template -- render it and look. + core = None + for candidate in (Path("/home/rackpi/projects/LEDMatrix"), + REPO.parent / "LEDMatrix"): + if (candidate / "web_interface" / "templates" / "v3" / "partials" + / "plugin_config.html").exists(): + core = candidate + break + if core is None: + print(" SKIP no LEDMatrix core checkout found (set LEDMATRIX_CORE)") + else: + import re + from jinja2 import DictLoader, Environment + source = (core / "web_interface" / "templates" / "v3" / "partials" + / "plugin_config.html").read_text(encoding="utf-8") + match = re.search( + r"(\{%\s*set property_order = schema\['x-propertyOrder'\].*?" + r"\{%\s*endfor\s*%\})", source, re.S) + check("the ordering loop is still in the shipped template", bool(match)) + if match: + # The loop only sorts keys into tiers, so append an emitter to see + # which keys it actually considered. + block = match.group(1) + "|{{ tiers.basic }}{{ tiers.advanced }}|" + env = Environment(loader=DictLoader({'f': block}), autoescape=True) + schema = {'properties': {'shown': {'type': 'string'}, + 'hidden': {'type': 'string'}}, + 'x-propertyOrder': ['shown']} + tiers = env.get_template('f').render(schema=schema).split('|')[1] + check("a listed property reaches the form (%s)" % tiers.strip(), + 'shown' in tiers) + check("a property absent from the order is dropped entirely", + 'hidden' not in tiers) + + print("\n%s" % ("FAILED: %d" % len(failures) if failures + else "All checks passed")) + return 1 if failures else 0 + + +if __name__ == "__main__": + sys.exit(main())