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>
This commit is contained in:
132
plugins/slides/PLAN-slide-manager.md
Normal file
132
plugins/slides/PLAN-slide-manager.md
Normal file
@@ -0,0 +1,132 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user