Extract slideshow into a plugin (blueprint-only display plugin)

Second core feature pulled into a plugin. Slides is the minimal plugin shape:
a blueprint only - no model, no AssetType, no nav (it is purely an API surface
consumed by the TV dashboard).

- plugins/slides/: manifest (api_prefix /api/slides, no deps), api/ (moved
  blueprint, contract-pure imports via shopdb.api, same /api/slides route so the
  TV dashboard is unchanged), plugin.py (get_blueprint, get_models -> []).
- Fixed a latent bug in the move: error path used ErrorCodes.SERVER_ERROR which
  does not exist -> ErrorCodes.INTERNAL_ERROR.
- De-cored: deleted shopdb/core/api/slides.py, removed from CORE_BLUEPRINT_NAMES
  + core/api/__init__ import/__all__. Registered in instance/plugins.json.

Pinned with a characterization test first; passes unchanged against the plugin
blueprint. 174 tests pass, naming green, app boots 8 bundled plugins, /api/slides
verified live.

Note: the shopfloor dashboard is NOT extractable - it is a frontend-only Vue view
aggregating the businessunits + notifications APIs, with no backend to move and no
frontend plugin system.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
cproudlock
2026-06-26 20:50:32 -04:00
parent 8ddf771b72
commit b6ec4cb577
10 changed files with 171 additions and 78 deletions

View File

@@ -0,0 +1,5 @@
"""Slides plugin package."""
from .plugin import SlidesPlugin
__all__ = ['SlidesPlugin']

View File

@@ -0,0 +1,5 @@
"""Slides plugin API."""
from .routes import slides_bp
__all__ = ['slides_bp']

View File

@@ -1,8 +1,9 @@
"""Slides API for TV dashboard slideshow."""
"""Slides API for the TV dashboard slideshow."""
import os
from flask import Blueprint, current_app
from shopdb.utils.responses import success_response, error_response, ErrorCodes
from shopdb.api import success_response, error_response, ErrorCodes
slides_bp = Blueprint('slides', __name__)
@@ -12,8 +13,7 @@ VALID_EXTENSIONS = {'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp'}
@slides_bp.route('', methods=['GET'])
def get_slides():
"""
Get list of slides for TV dashboard.
"""Get list of slides for the TV dashboard.
Returns image files from the static/slides directory.
"""
@@ -60,15 +60,12 @@ def get_slides():
slides.append({'filename': filename})
except Exception as e:
return error_response(
ErrorCodes.SERVER_ERROR,
ErrorCodes.INTERNAL_ERROR,
f'Error reading slides: {str(e)}',
http_code=500
)
# Determine base path for serving files
basepath = '/static/slides/'
return success_response({
'slides': slides,
'basepath': basepath
'basepath': '/static/slides/'
})

View File

@@ -0,0 +1,12 @@
{
"name": "slides",
"version": "1.0.0",
"description": "TV dashboard slideshow images served from a static folder",
"author": "ShopDB Team",
"dependencies": [],
"core_version": ">=0.1.0,<1.0.0",
"api_prefix": "/api/slides",
"provides": {
"features": ["slideshow"]
}
}

62
plugins/slides/plugin.py Normal file
View File

@@ -0,0 +1,62 @@
"""Slides plugin main class.
Display/service plugin: contributes a blueprint only (no model, no AssetType,
no nav). Serves TV-dashboard slideshow images. Demonstrates the minimal plugin
shape - a feature that is purely an API surface.
"""
import json
import logging
from pathlib import Path
from typing import List, Dict, Optional, Type
from flask import Flask, Blueprint
from shopdb.plugins.base import BasePlugin, PluginMeta
from .api import slides_bp
logger = logging.getLogger(__name__)
class SlidesPlugin(BasePlugin):
"""Slides plugin - TV dashboard slideshow images."""
def __init__(self):
self._manifest = self._load_manifest()
def _load_manifest(self) -> Dict:
"""Load plugin manifest from JSON file."""
manifest_path = Path(__file__).parent / 'manifest.json'
if manifest_path.exists():
with open(manifest_path, 'r') as f:
return json.load(f)
return {}
@property
def meta(self) -> PluginMeta:
"""Return plugin metadata."""
return PluginMeta(
name=self._manifest.get('name', 'slides'),
version=self._manifest.get('version', '1.0.0'),
description=self._manifest.get(
'description',
'TV dashboard slideshow images'
),
author=self._manifest.get('author', 'ShopDB Team'),
dependencies=self._manifest.get('dependencies', []),
core_version=self._manifest.get('core_version', '>=0.1.0,<1.0.0'),
api_prefix=self._manifest.get('api_prefix', '/api/slides'),
)
def get_blueprint(self) -> Optional[Blueprint]:
"""Return Flask Blueprint with API routes."""
return slides_bp
def get_models(self) -> List[Type]:
"""No models - slides are read from the filesystem."""
return []
def init_app(self, app: Flask, db_instance) -> None:
"""Initialize plugin with Flask app."""
logger.info(f"Slides plugin initialized (v{self.meta.version})")

View File

@@ -106,7 +106,6 @@ CORE_BLUEPRINT_NAMES = (
'reports',
'collector',
'employees',
'slides',
'settings',
'auditlogs',
'users',

View File

@@ -15,7 +15,6 @@ from .search import search_bp
from .reports import reports_bp
from .collector import collector_bp
from .employees import employees_bp
from .slides import slides_bp
from .settings import settings_bp
from .auditlogs import auditlogs_bp
from .users import users_bp
@@ -36,7 +35,6 @@ __all__ = [
'reports_bp',
'collector_bp',
'employees_bp',
'slides_bp',
'settings_bp',
'auditlogs_bp',
'users_bp',

View File

@@ -0,0 +1,14 @@
"""Characterization test for the slides (TV slideshow) endpoint.
Written before extracting slides into a plugin: /api/slides must behave
identically as a plugin blueprint (same prefix, same response shape).
"""
def test_slides_endpoint_shape(client, db):
"""GET /api/slides returns a slides list and a basepath (no auth required)."""
resp = client.get('/api/slides')
assert resp.status_code == 200
data = resp.get_json()['data']
assert isinstance(data['slides'], list)
assert 'basepath' in data

View File

@@ -17,7 +17,7 @@ from shopdb.plugins import plugin_manager
from shopdb.plugins.base import BasePlugin, PluginMeta
BUNDLED_PLUGINS = ('computers', 'equipment', 'knowledgebase', 'network', 'notifications', 'printers', 'usb')
BUNDLED_PLUGINS = ('computers', 'equipment', 'knowledgebase', 'network', 'notifications', 'printers', 'slides', 'usb')
@pytest.fixture

View File

@@ -99,6 +99,7 @@ def test_plugin_loader_discovers_bundled_plugins(app):
'network',
'notifications',
'printers',
'slides',
'usb',
}