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.
Employees plugin - directory database contract
Read-only lookups against a separate employee/HR directory database. Powers:
- Employee search (add people to a notification, browse the directory)
- Single + batch SSO lookup
- Recognition notifications (name + photo on the shopfloor / lobby displays)
The plugin never writes to this database. Use a read-only account.
This is the integration most likely to differ per site. HR / directory systems vary widely, so expect to map a site's schema to the contract below - the
CREATE VIEWrecipe at the end is the normal way to do it.
Connection
Credentials resolve settings-first, then environment, except the password which is env-only (never stored in the app database).
| Field | Setting key (editable in the setup wizard) | Env var (fallback) |
|---|---|---|
| Host | employee_db_host |
EMPLOYEE_DB_HOST |
| Database | employee_db_name |
EMPLOYEE_DB_NAME |
| User | employee_db_user |
EMPLOYEE_DB_USER |
| Password | (not stored) | EMPLOYEE_DB_PASSWORD |
Set the password in .env; the setup wizard's Features step shows the exact
line to paste. Engine is MySQL/MariaDB (pymysql).
Required schema
The plugin runs these queries verbatim, so a site's database must expose a
table (or view - see below) named employees with these columns:
| Column | Type | Notes |
|---|---|---|
SSO |
INT | Unique person id. Lookups require it to be numeric |
First_Name |
VARCHAR | Displayed as the given name |
Last_Name |
VARCHAR | Displayed as the surname; sort key |
Team |
VARCHAR | Team / group label |
Role |
VARCHAR | Job title / role |
Picture |
VARCHAR | Image filename (see Photos) |
Queries actually executed:
-- search
SELECT SSO, First_Name, Last_Name, Team, Role, Picture
FROM employees
WHERE First_Name LIKE %s OR Last_Name LIKE %s OR CAST(SSO AS CHAR) LIKE %s
ORDER BY Last_Name, First_Name LIMIT %s;
-- single / batch
SELECT SSO, First_Name, Last_Name, Team, Role, Picture FROM employees WHERE SSO = %s;
SELECT SSO, First_Name, Last_Name, Team, Role, Picture FROM employees WHERE SSO IN (...);
Rows are returned to the API with these exact column names. The frontend
(EmployeeSearch, NotificationForm, EmployeeDetail, ShopfloorDashboard) reads
SSO, First_Name, Last_Name, Team, Role, Picture as-is - do not
rename them in the response.
Photos
Picture holds an image filename (e.g. 123456.jpg), not a path or blob.
The app renders it as /static/employees/<Picture>, so the image files must
live in the app's static/employees/ directory. Leave Picture empty/NULL for
people with no photo; the UI falls back to initials.
Two ways to provide the directory
Option A - map an existing HR/directory database (see the view recipe below). Use this when the site already has a system of record for people.
Option B - self-hosted directory (managed in-app) for sites with no HR
database. Set employee_directory_mode to selfhosted and manage people under
Settings > Employee Directory (add / edit / delete + CSV import) - no SQL
needed. The app owns a directoryemployees table (migration 7d16); the lookup
APIs read it automatically in this mode.
If you would rather load it with SQL, the canonical table is equivalent to:
CREATE DATABASE shopdb_directory CHARACTER SET utf8mb4;
USE shopdb_directory;
CREATE TABLE employees (
SSO INT NOT NULL PRIMARY KEY,
First_Name VARCHAR(100) NOT NULL,
Last_Name VARCHAR(100) NOT NULL,
Team VARCHAR(100) NULL,
Role VARCHAR(100) NULL,
Picture VARCHAR(255) NULL
);
-- add people (or bulk-load from CSV with LOAD DATA INFILE)
INSERT INTO employees (SSO, First_Name, Last_Name, Team, Role, Picture)
VALUES (123456, 'Jane', 'Doe', 'Inspection', 'Quality Tech', '123456.jpg');
Put photo files (named as in Picture) under the app's static/employees/.
In self-hosted mode the directoryemployees table lives in the main app DB, so
no separate database is required.
Adapting a different site schema (recommended: a view)
Sites whose HR/directory database uses different table or column names should
not be forced to rename anything. Instead, create a read-only view
named employees that maps local columns to the names above:
CREATE VIEW employees AS
SELECT
person_id AS SSO,
given_name AS First_Name,
surname AS Last_Name,
department AS Team,
job_title AS Role,
photo_filename AS Picture
FROM hr_people
WHERE active = 1;
Grant the app's read-only user SELECT on the view. No app code changes -
point employee_db_* at that database and the plugin works.
Notes:
SSOmust be numeric (single/batch lookup validateisdigit()).- Column-name case follows your database's identifier casing; match the names above exactly on case-sensitive platforms.
- If the directory is unreachable or the
employeesobject is missing, lookups return a 500 and the rest of the app keeps working (the feature degrades, it does not crash the app).