# 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' | '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/<surface>/<filename>`  -> serve the image (content-type +
        path-traversal guard; send_file from instance/slides/<surface>/).
- 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.
