# Plan: Slides plugin -> full slide manager (Flask) Port the classic-ASP slide manager (tv-dashboard/slidemanager.asp + apislides.asp) into the shopdb-flask `slides` plugin. Two surfaces (lobby display, shopfloor screensaver), upload / reorder / delete, consumed by the lobby TV dashboard and the EventSaver screensaver over HTTP. Existing plugin is read-only (single-folder GET). This extends it to full CRUD + per-surface + a management UI. ## Decisions (locked for this plan) - SURFACES: fixed allowlist `lobby`, `shopfloor` (mirrors the ASP allowlist). Not user-defined - keeps validation + paths simple. - STORAGE: image FILES on disk, ORDER/metadata in the DB. - Files: `instance/slides//` (outside static; served via a plugin route so we control content-type + path-traversal, like slide.asp). - Metadata: one table (order + seconds + surface). Files are the source of truth for existence; DB rows that lost their file are ignored + pruned (same self-healing as GetOrderList in the ASP lib). - Rationale: native, simple, no BLOBs. BLOB-in-DB is the fallback only if the deploy host cannot give Flask a writable volume. - MIGRATION: add the table to the CORE alembic chain (ADR-004 / 7c04 - single authoritative chain, no per-plugin chain). - AUTH: admin/CRUD routes JWT-protected. The FEED + image-serve routes are PUBLIC (the screensaver + lobby kiosks have no auth - matches apislides.asp). - NAMING: locked convention v1 - lowercase concatenated table/columns; Python/JS vars mirror column names exactly. ## Data model (core migration) Table `tvslides` (new, added to core chain): | column | type | notes | |--------------|--------------|-----------------------------------------| | slideid | int PK | | | surface | varchar(20) | 'lobby' | 'shopfloor' (indexed) | | filename | varchar(255) | safe basename, unique per surface | | sortorder | int | play order within surface | | seconds | int | 0 = use default interval | | uploadeddate | datetime | | Unique (surface, filename). Model lives in `plugins/slides/models/tvslide.py`, imported via `shopdb.api` surface only (contract purity). Registered in the core migration chain per ADR-004. ## Backend - plugins/slides/ ``` plugins/slides/ plugin.py # SlidesPlugin: get_blueprint, get_models, get_navigation_items, on_install manifest.json # api_prefix /api/slides, provides slideshow + slidemanager models/tvslide.py # TvSlide model api/routes.py # blueprint slides_bp ``` Routes (prefix `/api/slides`): - PUBLIC (no auth) - consumed by screensaver + lobby: - `GET /feed?surface=lobby|shopfloor` -> flat shape the EventSaver .scr + lobby expect: `{success, surface, basepath, interval, slides:[{filename, seconds}]}` basepath -> the image route below. (Deliberately NOT the success_response {data:{}} wrapper, so the .scr parser needs no change.) - `GET /img//` -> serve the image (content-type + path-traversal guard; send_file from instance/slides//). - ADMIN (JWT) - consumed by the Vue manager: - `POST //upload` -> request.files (native multipart), save + create TvSlide rows at end of order. Skips non-images. Unique-renames. - `POST //order` -> body {order:[filename,...]} rewrite sortorder, preserve seconds. - `POST //delete` -> body {files:[...]} delete file + row (multi). - `PATCH //` -> {seconds} (kept server-side; UI hidden for now). - `GET /` -> admin list (ordered, with slideid) for the UI. Ordering/natural-sort: order comes from `sortorder`; newly-uploaded files append in natural (numeric-aware) filename order so Slide1..Slide11 land right (port the ASP NatKey). ## Frontend - core (no frontend plugin system) Vue pages live in core `frontend/src/` (per project note - plugins own backend, Vue pages are core). Add: - `frontend/src/views/SlideManager.vue` - Surface tabs (Lobby Display / Shopfloor Screensaver). - Upload (file input -> POST /upload), thumbnail grid/table. - Drag reorder via vuedraggable -> POST /order. - Checkbox multi-select + Delete Selected -> POST /delete. - Inherits AppLayout (sidebar + topbar + theme) automatically -> matches the site with ZERO styling work (the whole reason to move off ASP). - Router entry in `frontend/src/router` (e.g. /slides), guarded (admin). - Nav: plugin `get_navigation_items()` returns the sidebar entry; the frontend nav consumes plugin nav hooks (as knowledgebase/notifications do). - `frontend/src/api/slidesApi.js` wrapper for the admin calls. ## Consumers - Lobby: `TVDashboard.vue` already hits /api/slides; repoint at `/feed?surface=lobby`. - Screensaver: change `EventSaver.ini` `url=` to `https:///api/slides/feed?surface=shopfloor`, rehash the ini + bump the manifest DetectionValue. The .scr HTTP mode is unchanged because /feed returns the flat shape it already parses. ## Tests (pytest, per plugin conventions) - test_plugins/test_slides.py: upload (fake file) creates rows + file; feed returns ordered flat shape; order persists + preserves seconds; delete removes file+row; surface allowlist rejects junk; path-traversal guard on /img; natural sort on append; feed is public / admin routes require JWT. - Guard test already enforces contract-only imports. ## Phasing 1. Model + core migration + manifest bump. 2. Backend routes (feed + img public; upload/order/delete/list admin) + tests. 3. Vue SlideManager.vue + router + nav + slidesApi.js. 4. Repoint TVDashboard.vue to /feed?surface=lobby. 5. Cut over screensaver: EventSaver.ini url -> Flask /feed, rehash + manifest. 6. Retire classic ASP tv-dashboard slide pages once Flask is live for this site. ## Open decisions / gates - DEPLOY GATE: shopdb-flask must be deployed + reachable by the screensaver PCs and lobby, with a writable `instance/slides` volume. Live shopdb is still the classic ASP box; Flask prod target is docker, not yet deployed here. This plan is buildable now but not cutover-able until Flask is live. - Image serving: via a plugin route (send_file, guarded) vs Flask static. Route chosen for the traversal guard + content-type control (parity with slide.asp). - Seconds field: model + PATCH kept; UI control hidden for now (matches the ASP decision to hide per-slide delay). - BLOB fallback: only if the host denies a writable slides volume.