Files
shopdb-flask/plugins/slides/PLAN-slide-manager.md
Cam Proudlock a9d0dfff4a Initial import: shopdb-flask multi-site asset platform
Flask + Vue rewrite of the classic-ASP shopdb, built as a plugin
framework so sister GE Aerospace sites can adopt it. Product version
0.7.0-pre-0.8.0, plugin contract 0.11.0, 966 backend tests.

Key entry points:
- docs/INSTALL-WINDOWS-IIS.md - tested Windows + IIS install runbook
  (own-site and subpath deployment methods)
- docs/PILOT-DEPLOY.md - production pilot: stand up + import + verify
- docs/IMPORT-API.md - schema-agnostic legacy data import contract
- scripts/site_imports/wjf/ - West Jefferson reference import loader
- docs/adr/ - twelve accepted architecture decision records
2026-07-14 07:58:51 -04:00

6.6 KiB

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/<surface>/<filename> (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'
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/<surface>/<filename> -> serve the image (content-type + path-traversal guard; send_file from instance/slides//).
  • ADMIN (JWT) - consumed by the Vue manager:
    • POST /<surface>/upload -> request.files (native multipart), save + create TvSlide rows at end of order. Skips non-images. Unique-renames.
    • POST /<surface>/order -> body {order:[filename,...]} rewrite sortorder, preserve seconds.
    • POST /<surface>/delete -> body {files:[...]} delete file + row (multi).
    • PATCH /<surface>/<slideid> -> {seconds} (kept server-side; UI hidden for now).
    • GET /<surface> -> 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://<flask-host>/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.