Files
shopdb-flask/plugins/slides/PLAN-slide-manager.md
cproudlock 78a0ee8d83 Add custom fields + warranty plugin, rework settings into two-pane shell
Feature work from the 2026-07 session:

Settings IA
- Replace the flat 27-card settings hub with a persistent two-pane shell
  (SettingsLayout.vue): grouped, searchable left rail + content pane.
- Nest all settings/* routes under the shell via router post-processing;
  shared nav catalog in settingsNav.js. Group by asset class (PCs, Printers,
  Equipment, Network) so per-type settings stop scattering.

Custom fields (core)
- customfields + customfieldvalues tables (migration 7d14), CRUD API at
  /api/customfields, per-asset value get/save.
- Settings management page + reusable CustomFieldsSection (detail) and
  CustomFieldsInputs (form) wired into all four asset types.

Warranty (new plugin)
- plugins/warranty: warranties + warrantyassets (migration 7d15), derived
  coverage status, provider abstraction (manual now; Dell/Lenovo/HP stubs).
- API CRUD + per-asset panel + report buckets; WarrantyPanel on all four
  detail pages; Warranties management page; Warranty report + Reports card.
- Seed warranty.* permissions.

Printer drivers
- printerdrivers table (migration 7d13) linked to printer models; drivers now
  surface on the matching printer's detail page.

Other
- PCDetail rebalanced (Network + Status + Warranty + custom fields on the right).
- Rename PCs list "Features" column to "Remote Access"; fix badge hover underline.
- Drop equipment islocationonly field.
- Centralize asset-type label/route maps into utils/assetTypes.js.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 15:37:21 -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.