refactor: remove the skin system and the unused src/base_classes package (#615)

* refactor: remove the skin system

Skins never rendered with the current scoreboard plugins: the only hook was
SportsCore._render_game in src/base_classes, which no plugin builds on, so
the UI and store already treated them as unsupported. The owner decided on
2026-09-23 to remove them outright.

Removed src/skin_system/ (runtime, base class, fixtures), skins/,
scripts/validate_skin.py and their tests; the store's "type": "skin"
installer, uninstaller and hide/refuse filters (the official registry lists
no skins); SchemaManager.inject_skin_selector; and GET /api/v3/skins.

Stored skin/skin_options config values are handled in the next commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(config): drop retired skin/skin_options keys instead of validating them

A config.json written while the skin system existed can carry skin and
skin_options in any plugin section, and most plugin schemas set
additionalProperties: false. They are no longer core plugin properties;
RETIRED_PLUGIN_KEYS in schema_manager lists them and
drop_retired_plugin_keys removes them (unless the plugin's own schema
declares the name) in prepare_plugin_config, which loading, hot reload,
GET /plugins/config and both web saves already share, and in
validate_config_against_schema for callers that validate a raw section.
POST /plugins/config and /config/main also drop them from the stored
section they merge into, so they leave config.json on the next save.

Tests cover the load path (real PluginManager.load_plugin: no schema
warning, not degraded), raw and prepared validation,
validate_all_plugin_configs, and the JSON, form and /config/main saves.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor: remove the unused src/base_classes package

No scoreboard plugin builds on src.base_classes: the nine monorepo
scoreboards ship their own sports.py and share code through src/common
(docs/SPORTS_UNIFICATION.md), and none of the third-party registry plugins
imports it. The one import anywhere, baseball-scoreboard's
rankings_manager.py, is a lazy import of ESPNDataSource in a class nothing
instantiates.

Removed the package and the eight test files that only tested it
(test_api_extractors, test_data_sources, test_sports_base_characterization,
test_sports_capabilities, test_sports_core_promotions,
test_sports_logo_cache_bounded, test_sports_modes_promotions,
test_sports_odds_fanout). test_common_is_hardware_free no longer lists
src.base_classes as a forbidden import, and comments in sports_helpers.py
and base_odds_manager.py stop pointing at it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs: drop the skin system and src/base_classes from the docs

Deletes docs/SKIN_SYSTEM.md and docs/CREATING_SKINS.md and every link to
them (docs/README.md, README.md, PLUGIN_DEVELOPMENT_GUIDE.md, the /skins
section of REST_API_REFERENCE.md), the skin section of CLAUDE.md and the
term in PRODUCT.md. SPORTS_UNIFICATION.md now says src/base_classes was
removed and shared code lives in src/common, in the Layering section and
the view-model-contract rule. Other docs stop pointing at the removed
package. CHANGELOG records both removals under Unreleased.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(store): hide and refuse registry entries that aren't plugins

The skin filters went with the skin system, but a custom registry can still
list "type": "skin" entries, and installing one as a plugin would unpack it
into the plugins directory. PluginStoreManager.is_plugin_entry() (a missing
type means plugin) now hides non-plugin entries from the store and
custom-registry listings, and install refuses them, in the route with a
clear 400 and in _install_plugin_impl for any other caller.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Chuck
2026-09-23 16:33:24 -04:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 4fe3cdd906
commit 61e462c635
75 changed files with 431 additions and 12560 deletions
+2 -3
View File
@@ -1261,9 +1261,8 @@ def _enhance_schema_with_core_properties(schema):
"""
Enhance schema with the core-owned per-plugin properties.
``enabled``, ``display_duration``, ``live_priority``, ``skin``,
``skin_options`` and the ``vegas_*`` tuning keys are system-managed and
always allowed, even when the plugin's schema doesn't declare them. The
``enabled``, ``display_duration``, ``live_priority`` and the ``vegas_*``
tuning keys are system-managed and always allowed, even when the plugin's schema doesn't declare them. The
list is ``schema_manager.CORE_PLUGIN_PROPERTIES``, the one validation uses,
so the save filter keeps exactly what validation accepts.
+6 -2
View File
@@ -1056,10 +1056,14 @@ def save_main_config():
if error:
return error
# Deep merge regular config into main config
# Deep merge regular config into main config, dropping
# retired core keys (skin, skin_options) from the stored section
from src.plugin_system.schema_manager import drop_retired_plugin_keys
stored_section = current_config.get(plugin_id)
current_config[plugin_id] = deep_merge(
stored_section if isinstance(stored_section, dict) else {}, regular_config)
drop_retired_plugin_keys(
stored_section if isinstance(stored_section, dict) else {}, schema),
regular_config)
if secrets_config:
plugin_secrets_updates[plugin_id] = secrets_config
+1 -46
View File
@@ -1,5 +1,5 @@
"""Routes with no larger group of their own: errors, integrations,
cache, sync, skins, logs, health and hardware.
cache, sync, logs, health and hardware.
Routes decorate the shared `api_v3` Blueprint from ._common, so their
endpoint names are unchanged by living here.
@@ -150,51 +150,6 @@ def get_hardware_status():
except Exception:
logger.error("Unexpected error reading hardware status", exc_info=True)
return jsonify({"status": "error", "message": "Unable to read hardware status"}), 500
@api_v3.route('/skins', methods=['GET'])
def list_skins():
"""List installed visual skins (docs/SKIN_SYSTEM.md).
Optional ?plugin_id=... filters to skins matching that plugin.
The response carries ``supported: false`` and a ``message``: the current
scoreboard plugins don't render skins, so a client must not present
these as selectable.
"""
try:
from src.skin_system import (
SKINS_RENDER_SUPPORTED, SKINS_UNSUPPORTED_MESSAGE, skin_runtime,
)
plugin_id = request.args.get('plugin_id')
if plugin_id:
skins = skin_runtime.skins_for_plugin(plugin_id)
else:
# The discovery cache self-invalidates on directory/manifest
# mtime changes, so no force_refresh — keeps Pi disk I/O down.
skins = skin_runtime.discover_skins()
payload = []
for skin_id, manifest in sorted(skins.items()):
skin_dir = Path(manifest['_skin_dir'])
preview = manifest.get('preview')
payload.append({
'id': skin_id,
'name': manifest.get('name', skin_id),
'version': manifest.get('version'),
'author': manifest.get('author'),
'description': manifest.get('description', ''),
'skin_api_version': manifest.get('skin_api_version'),
'targets': manifest.get('targets', {}),
'modes': manifest.get('modes', []),
'has_preview': bool(preview and (skin_dir / preview).is_file()),
})
data = {'skins': payload, 'supported': SKINS_RENDER_SUPPORTED}
if not SKINS_RENDER_SUPPORTED:
data['message'] = SKINS_UNSUPPORTED_MESSAGE
return jsonify({'status': 'success', 'data': data})
except Exception as e:
logger.error('Error in list_skins', exc_info=True)
return jsonify({'status': 'error', 'message': 'An error occurred; see logs for details', 'details': describe_exception(e)}), 500
@api_v3.route('/logs', methods=['GET'])
def get_logs():
"""Get system logs from journalctl"""
+13 -19
View File
@@ -1296,15 +1296,16 @@ def install_plugin():
plugin_id = data['plugin_id']
branch = data.get('branch') # Optional branch parameter
# A registry skin would install but never render with the current
# scoreboards; refuse it with the reason rather than a generic failure
# A registry entry that isn't a plugin (a custom registry can still
# list old "type": "skin" entries) gets a clear refusal, not a failed
# install.
try:
registry_entry = api_v3.plugin_store_manager.get_registry_info(plugin_id)
except Exception:
registry_entry = None
if isinstance(registry_entry, dict) and (registry_entry.get('type') or 'plugin') == 'skin':
from src.skin_system import SKINS_UNSUPPORTED_MESSAGE
return jsonify({'status': 'error', 'message': SKINS_UNSUPPORTED_MESSAGE}), 400
if isinstance(registry_entry, dict) and not api_v3.plugin_store_manager.is_plugin_entry(registry_entry):
return jsonify({'status': 'error',
'message': f"{plugin_id} is a {registry_entry.get('type')!r} entry, not a plugin"}), 400
# Install the plugin
# Log the plugins directory being used for debugging
@@ -1506,13 +1507,10 @@ def get_registry_from_url():
registry = api_v3.plugin_store_manager.fetch_registry_from_url(repo_url)
if registry:
# Skins aren't offered: current scoreboards don't render them
return jsonify({
'status': 'success',
'plugins': [
p for p in registry.get('plugins', [])
if not (isinstance(p, dict) and (p.get('type') or 'plugin') == 'skin')
],
'plugins': [p for p in registry.get('plugins', [])
if api_v3.plugin_store_manager.is_plugin_entry(p)],
'registry_url': repo_url
})
else:
@@ -1627,9 +1625,7 @@ def list_plugin_store():
# Format plugins for the web interface
formatted_plugins = []
for plugin in plugins:
# Registry skins install but never render with the current
# scoreboards, so the store doesn't offer them
if (plugin.get('type') or 'plugin') == 'skin':
if not api_v3.plugin_store_manager.is_plugin_entry(plugin):
continue
formatted_plugins.append({
'id': plugin.get('id'),
@@ -2210,7 +2206,10 @@ def save_plugin_config():
if plugin_id not in current_config:
current_config[plugin_id] = {}
current_config[plugin_id] = deep_merge(current_config[plugin_id], regular_config)
# Retired core keys (skin, skin_options) leave the stored section here
from src.plugin_system.schema_manager import drop_retired_plugin_keys
current_config[plugin_id] = deep_merge(
drop_retired_plugin_keys(current_config[plugin_id], schema), regular_config)
# Deep merge plugin secrets in secrets config
if secrets_config:
@@ -2755,11 +2754,6 @@ def get_plugin_schema():
schema = schema_mgr.load_schema(plugin_id, use_cache=True)
if schema:
# No "Visual Skin" dropdown: the current scoreboard plugins don't
# render skins (src.skin_system.SKINS_UNSUPPORTED_MESSAGE), so
# schema_mgr.inject_skin_selector is deliberately not called. A
# stored "skin" value is unaffected: validation still allows it
# and a form save deep-merges over the stored section, keeping it.
return jsonify({'status': 'success', 'data': {'schema': schema}})
# Return a simple default schema if file not found