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

@@ -0,0 +1,71 @@
"""Slides API for the TV dashboard slideshow."""
import os
from flask import Blueprint, current_app
from shopdb.api import success_response, error_response, ErrorCodes
slides_bp = Blueprint('slides', __name__)
# Valid image extensions
VALID_EXTENSIONS = {'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp'}
@slides_bp.route('', methods=['GET'])
def get_slides():
"""Get list of slides for the TV dashboard.
Returns image files from the static/slides directory.
"""
# Look for slides in static folder
static_folder = current_app.static_folder
if not static_folder:
static_folder = os.path.join(current_app.root_path, 'static')
slides_folder = os.path.join(static_folder, 'slides')
# Also check frontend public folder
frontend_slides = os.path.join(
os.path.dirname(os.path.dirname(os.path.dirname(current_app.root_path))),
'frontend', 'public', 'slides'
)
# Try multiple possible locations
possible_paths = [
slides_folder,
frontend_slides,
'/home/camp/projects/shopdb-flask/shopdb/static/slides',
'/home/camp/projects/shopdb-flask/frontend/public/slides',
]
slides_path = None
for path in possible_paths:
if os.path.isdir(path):
slides_path = path
break
if not slides_path:
return success_response({
'slides': [],
'basepath': '/static/slides/',
'message': 'Slides folder not found'
})
# Get list of image files
slides = []
try:
for filename in sorted(os.listdir(slides_path)):
ext = os.path.splitext(filename)[1].lower()
if ext in VALID_EXTENSIONS:
slides.append({'filename': filename})
except Exception as e:
return error_response(
ErrorCodes.INTERNAL_ERROR,
f'Error reading slides: {str(e)}',
http_code=500
)
return success_response({
'slides': slides,
'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})")