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>
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).