An SVG is an XML document that may carry a script, and it is an accepted image
type because floor-plan maps and branding genuinely want vector. Loaded through
an img tag that script never runs, so the tiles and maps were never the risk.
Opening the file's own URL is - and the application image route is public, so
that URL needs no session.
Every route that serves an upload now goes through one helper that sends
Content-Security-Policy: default-src 'none'; sandbox, and nosniff. Seven routes
across core and five plugins, so a new one added later starts from the same
place rather than repeating the reasoning. Banning the format instead would
have cost the maps their only sensible one.
The app also sent no security headers at all. It now sets nosniff,
frame-ancestors self (as X-Frame-Options too, for the display bays' browsers)
and a referrer policy. Deliberately NOT a page-wide CSP: this serves an SPA with
inline styles, so a real script-src policy is a change worth making with the
frontend in front of you, and a permissive header claiming one would be worse
than having none.
Contract 0.19.0. send_upload is on the shopdb.api surface, because a plugin
serving user-supplied bytes should not have to remember these headers. The same
bump records that get_dashboard_widgets has taken data and shape rather than a
component name since the dashboard was rebuilt - that shipped without a bump,
while BasePlugin and PLUGIN-HOOKS.md both still documented the shape nothing
renders, which is how five plugins came to declare widgets pointing at
components nobody had written.
The lobby-display and screensaver slide manager was admin-only. Add a shared
slides.manage permission so a curator can manage both surfaces without full
admin. Admins keep access via the require_permission admin bypass.
Backend:
- plugins/slides/api/routes.py: all 5 management routes require slides.manage
- plugins/slides/plugin.py: declare it via get_permissions(); nav item carries
the permission so the frontend can gate visibility
- shopdb/core/api/auth.py: login response now returns the user's permissions
(matches /me) so the frontend authStore has them on fresh login
Frontend:
- stores/auth.js: hasPermission(name) getter (admin true, else granted list)
- router/index.js: guard supports requiresPermission
- views/AppLayout.vue: hide nav items whose permission the user lacks
- plugins/slides/frontend/routes.js: slide manager gated requiresPermission
Tests: no-perm user 403, curator role with the perm 200 (+ login advertises
it), admin 200 via bypass.
Deploy: run `flask seed permissions` to create the row, then grant it to a
role in Settings > Users & Roles.
Make the app distributable to other GE Aerospace sites (one self-hosted
instance per site, ADR-004). GE values remain the shipped defaults; every
site-specific behavior is now a Setting an admin can change in the UI.
Settings-driven site config:
- Branding: site/QR/badge logos, favicon, primary color (upload endpoints
mirror the map-blueprint pattern; new Settings > Branding section).
- ServiceNow: search/incident/change URL templates ({ticket}), ticket
prefixes, enable toggle. Defaults point at the current
geaerospaceqa.service-now.com global search. Disabled = plain-text tickets.
- Employee-id regex (employeeid_pattern), printer hostname template,
QR label targets (qr_target_printer / qr_target_usb, blank = asset page,
else URL template with placeholders), usb_label_style (barcode|qr).
- West Jefferson floor-plan PNGs removed from the tree; generic placeholder
ships as the map default and sites upload their own blueprint.
Security closeout:
- dashboarddefaults writes now require admin.
- Collector: generic error messages (no str(exc) leak); API key accepted
via X-API-Key header only (BREAKING: querystring api_key removed).
- IP-based login rate limiting (AUTH_RATELIMIT_* knobs) atop account lockout.
- Setting.set() creation race fixed (IntegrityError retry).
Release engineering and docs:
- __version__ 0.5.0 (distinct from __contract_version__, ADR-007),
CHANGELOG.md, Gitea Actions CI config, frontend version aligned.
- One wizard-first install story across README/DEPLOY; new CONFIG.md,
UPGRADE.md, BACKUP-RESTORE.md; CLAUDE.md and ROADMAP de-staled.
- Dockerfile multi-stage build now bundles the frontend; compose binds
MySQL to 127.0.0.1; stale database/schema.sql and one-off SQL removed.
Debt and fixes:
- .query.get() -> db.session.get() sweep; datetime.utcnow() removed
(naive-UTC via timezone-aware now); users.py on authz decorators.
- Fixed 4 stale tests (slides feed shape, shopfloor splitperemployee,
plugin contract purity) and the USB label page field mapping (both usb
modes emit the cmmc shape: device_id/device_desc).
- Health endpoint reports the real version.
248 tests pass; naming/style check green; frontend builds; fresh-DB
flask db upgrade + seeds verified; QR targets verified by decoding
rendered codes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
Second core feature pulled into a plugin. Slides is the minimal plugin shape:
a blueprint only - no model, no AssetType, no nav (it is purely an API surface
consumed by the TV dashboard).
- plugins/slides/: manifest (api_prefix /api/slides, no deps), api/ (moved
blueprint, contract-pure imports via shopdb.api, same /api/slides route so the
TV dashboard is unchanged), plugin.py (get_blueprint, get_models -> []).
- Fixed a latent bug in the move: error path used ErrorCodes.SERVER_ERROR which
does not exist -> ErrorCodes.INTERNAL_ERROR.
- De-cored: deleted shopdb/core/api/slides.py, removed from CORE_BLUEPRINT_NAMES
+ core/api/__init__ import/__all__. Registered in instance/plugins.json.
Pinned with a characterization test first; passes unchanged against the plugin
blueprint. 174 tests pass, naming green, app boots 8 bundled plugins, /api/slides
verified live.
Note: the shopfloor dashboard is NOT extractable - it is a frontend-only Vue view
aggregating the businessunits + notifications APIs, with no backend to move and no
frontend plugin system.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>