Files
shopdb-flask/docs/CONFIG.md
cproudlock 534f179ee6 Make main the lean product a site receives
main was 16 days and 259 commits behind the line the product is actually built
on, at contract 0.30.0 against 0.35.0, with no per-PC credential code at all. It
was neither current nor curated - just stale. This brings it forward and, at the
same time, makes it mean something: main is what a receiving site installs, and
nothing else.

WHAT IT CARRIES. Twelve plugins - backups, computers, endpoints, guides,
knowledgebase, machines, network, notifications, printers, slides, tools,
warranty - plus applications, which is manifest-less core and always ships. The
only manifest dependency inside that set is endpoints on computers, and both are
in it, so the closure is self-contained.

WHAT IT DOES NOT. employees, geenforce, measuringtools, printedparts and usb
stay on the site line. So do the Windows installer tree, the VM rehearsal
helpers, the agent tooling config, one site's data loader, the internal audit
and findings records, and the planner spreadsheet. 1,135 tracked files against
1,438.

A REMOVAL THAT BREAKS THE APP IS NOT A REMOVAL, and two nearly shipped:

deploy/site-profile-universal.json still named all seventeen plugins. It is the
file docs/DEPLOY.md tells a site to provision from, so a fresh install would
have failed at `flask plugin apply-profile` with PluginError - the first thing a
receiving site does, broken by the branch meant to serve them. It now names the
twelve that exist, and its comment says the two must agree.

agent/ was never refreshed. Whoever populated this branch copied shopdb/,
plugins/, frontend/ and tests/ from the site line and left the agent tree at its
September state: 22 C# and PowerShell sources missing, including AgentCredentials,
EnrollmentService, AgentHttpFactory and the fleet-alert client, plus one file the
site line had deleted still present. The old tree is internally consistent, so it
compiles, and nothing in CI builds it - every gate was green over a server that
speaks enrollment and per-PC credentials to a bundled agent source that cannot.
Resynced whole.

DECISIONS THIS ENCODES, rather than defers:

- The plugin-authoring lab builds printedparts, whose manifest depends on
  employees, so it could not stay. The lab remains on the site line and main
  ships no build-along tutorial. That is a real loss for an adopting developer
  and is recorded here rather than discovered later.
- Recognition, Training and Recertification notifications are no longer seeded.
  Each splits one notification into a card per employee and renders a photo,
  which needs a plugin this branch does not bundle. Seeding them would offer an
  author a type that cannot resolve anybody. A site that installs employees gets
  them from that plugin's own seed.
- Windows/IIS is documented as the legacy path throughout, and the README leads
  with Docker, because that is what a receiving site gets.

The API description is regenerated for THIS branch - 347 paths and 483
operations, down from 415 and 573 - so the published contract describes what is
here rather than what the site line serves. docs/CONFIG.md lost the two
measuring-tool label keys the code no longer declares, which two lanes had
decided opposite ways.

Suite: 3124 passed, 6 skipped. Naming and style gates pass. The docs gates run
rather than skip, and every index line count is derived from the file it names.
2026-09-19 11:24:40 -04:00

20 KiB

Configuration reference

To find one setting: search this page for its exact key. Every key appears in exactly one table. Nothing here has to be read in order.

Two places, and the split is deliberate:

  • Environment variables hold secrets and deploy-time wiring: database credentials, signing keys, CORS origins, ports, API keys. Read once at boot by shopdb/config.py, which is authoritative - where this page and that file disagree, the file wins.
  • The Settings table holds site preferences: branding, ServiceNow links, search toggles, facility identity. Seeded by flask seed settings, edited in the UI under Settings or the setup wizard, changeable at runtime with no restart. build_default_settings in shopdb/core/api/settings.py is authoritative for the defaults.

Rule of thumb: if leaking it would be a security incident, it is an environment variable. Never put a secret in the Settings table.

Part 1, environment variables: Flask core | Database | Auth rate limiting | Collector ingest | Zabbix | Employee directory DB | docker-compose only

Part 2, Settings keys: site (facility name, timezone, base URL, retention) | branding | printing (QR and label targets) | map (superseded) | integrations (ServiceNow, Zabbix, Dell warranty) | email | audit | auth (SAML placeholders) | tools (Secure Send) | identifiers | search | Custom fields


Part 1: Environment variables

Copy .env.example to .env and fill in values. In production (FLASK_ENV=production), ProductionConfig.validate() refuses to boot if SECRET_KEY, JWT_SECRET_KEY, DATABASE_URL, or CORS_ORIGINS are missing or set to the dev defaults.

Flask core

Variable Required Default Notes
FLASK_APP No wsgi.py Entry point for the flask CLI.
FLASK_ENV Yes development production for live sites (triggers validate()). Other values: development, testing.
SECRET_KEY Yes (prod) dev default Flask session/signing key. Generate: python -c "import secrets; print(secrets.token_urlsafe(64))".
JWT_SECRET_KEY Yes (prod) dev default JWT signing key. Different value from SECRET_KEY.
JWT_ACCESS_TOKEN_EXPIRES No 3600 Access-token TTL in seconds.
JWT_REFRESH_TOKEN_EXPIRES No 2592000 Refresh-token TTL in seconds (30 days).
CORS_ORIGINS Yes (prod) http://localhost:5173 Comma-separated explicit origins. Wildcard * is rejected in production.
LOG_LEVEL No INFO Logging verbosity.

Database

Variable Required Default Notes
DATABASE_URL Yes (prod) dev localhost URL mysql+pymysql://<user>:<pass>@<host>:<port>/<db>?charset=utf8mb4. Keep ?charset=utf8mb4.

Authentication rate limiting

IP-based fixed-window limit on the login endpoint, defense-in-depth atop the per-account lockout. Uses the cache extension, which is per-process, so the limit is approximate across multiple gunicorn workers.

Variable Required Default Notes
AUTH_RATELIMIT_ENABLED No True Set False to disable (TestingConfig disables it).
AUTH_RATELIMIT_MAX No 30 Max login attempts per source IP per window before 429.
AUTH_RATELIMIT_WINDOW_SECONDS No 300 Window length in seconds.

Collector ingest (ADR-006)

Variable Required Default Notes
COLLECTOR_API_KEY No (empty) Shared key for /api/collector/*. Endpoint fails closed (denies) when unset and no managed token is presented. Sent as the X-API-Key header.
COLLECTOR_API_KEY_<PLUGIN> No (empty) Per-plugin override, e.g. COLLECTOR_API_KEY_COMPUTERS. Checked before the shared key.

These env keys are the bootstrap/legacy fallback. A managed API token scoped to collector.ingest is preferred and is minted, rotated and revoked from Settings > API Tokens; see COLLECTOR-INTEGRATION.md (Managed collector tokens).

Zabbix (printer supply monitoring)

Variable Required Default Notes
ZABBIX_ENABLED No false Enable the Zabbix integration.
ZABBIX_URL No (empty) Zabbix API URL.
ZABBIX_TOKEN No (empty) Zabbix API bearer token.

These are the boot-time wiring; the Settings entries (zabbix_enabled, zabbix_url, zabbix_token) are preferred for runtime changes.

Employee directory database (optional, read-only)

Separate HR lookup DB, read-only. This is where staff names come from: global search resolves the employee domain through it, and the audit log uses it to show a name instead of a bare SSO. Nothing here is required - leave it unset and both surfaces fall back to SSOs. There is no safe default for the password; an unset one fails loud rather than trying a guessed credential.

Variable Required Default Notes
EMPLOYEE_DB_HOST No localhost HR DB host.
EMPLOYEE_DB_USER No (empty) HR DB user.
EMPLOYEE_DB_PASSWORD No (empty) HR DB password. No safe default.
EMPLOYEE_DB_NAME No wjf_employees HR DB name.

docker-compose only

Read by docker-compose.yml, not by the Flask app directly.

Variable Required Default Notes
MYSQL_ROOT_PASSWORD Yes (none) Root password for the bundled MySQL container.
MYSQL_PASSWORD Yes (none) App-user password; must match the DATABASE_URL password.
MYSQL_PORT No 3306 Host port for MySQL. Bound to 127.0.0.1 only.
API_PORT No 5001 Host port for the API container.

The opt-in backup overlay adds RESTIC_REPOSITORY, RESTIC_PASSWORD, SHOPDB_SITE and optional cloud credentials - see BACKUP-RESTORE.md.


Part 2: Settings table keys

Seeded by flask seed settings (idempotent; re-running adds anything missing). Values are stored as strings and typed by valuetype. Secrets in this table (anything whose key contains password, token, or secret) are masked when read back through the API.

site

Key Default Notes
setup_complete false Set true once the first-run wizard finishes; gates the /setup route.
employee_directory_mode selfhosted external reads a separate HR database (see EMPLOYEE_DB_*). No plugin on this line keeps a self-hosted directory, so selfhosted resolves no names - set external, or accept bare SSOs.
audit_retentiondays 0 Delete audit entries older than this many days. 0 keeps everything forever. Saving a period deletes nothing by itself: purge from this page or run flask audit purge --apply.
site_base_url (empty) Public base URL for QR codes and absolute links. Scheme + host, optionally including the subpath a site is served under (https://host/shopdb). Blank = use the browsing origin.
facility_name (empty) Facility name in the dashboard header. Blank = frontend falls back to ShopDB.
pc_access_domain device.geaerospace.net Domain appended to a PC hostname for remote-access links. Blank = hostname as-is.
employeeid_pattern ^\d{9}$ Regex a search term must match to be treated as an employee id. Invalid regex falls back to the default and never 500s.
printer_hostname_template Printer-{ip}.printer.geaerospace.net Printer hostname template. {ip} is the dash-separated IP address.
contact_email_domain geaerospace.com Email domain appended to a support contact's SSO to build email (sso@domain) and Teams-chat links. Blank hides the contact action buttons.
dualpath_single_machine true Treat a Dualpath pair (a dual-bay machine with one controller) as a single machine in the machines list, dashboard/report counts, and the floor map (the secondary bay is hidden). The data model always keeps both bay records; detail pages stay per-bay with a sibling banner. false lists and counts both bays separately.
site_timezone America/New_York IANA timezone for the site. Notification times are stored UTC and converted for display by frontend/src/utils/datetime.js, so they are entered and shown in THIS zone, not the viewer's browser zone; daily-reset notification expiry (expirymode=dailytime) is computed here too. Public-readable so kiosks can resolve it.

branding

Blank values fall back to the shipped GE default asset, so an un-reconfigured install still renders. Upload replacements at Settings > Branding, which saves them under instance/branding/.

Key Default Notes
site_logo /ge-aerospace-logo.svg Header and login-page logo.
qr_logo /ge-monogram.svg Logo composited in printer QR labels. Blank = no overlay.
badge_logo /ge-aerospace-logo.svg Logo on the machine badge print page.
site_favicon (empty) Browser tab favicon. Blank = shipped /favicon.svg.
brand_primary_color (empty) Primary brand color as a CSS color value (maps to --primary). Blank = built-in theme color.
brand_primary_dark_color (empty) Primary hover/active color (maps to --primary-dark). Blank = auto-derived by darkening the primary color ~15%.
brand_accent_color (empty) Accent color for secondary buttons and badges (maps to --secondary). Blank = built-in theme color.
brand_sidebar_color (empty) Sidebar background color (maps to --sidebar-bg). Blank = built-in theme color.

printing

Key Default Notes
qr_target_printer (empty) Custom URL template for printer QR labels. Blank = link to the printer page on this instance. Placeholders: {printerid}, {assetid}, {assetnumber}, {serialnumber}, {ip}, {hostname}.
qr_target_machine (empty) Custom URL template for machine labels. Blank = link to the machine page. Placeholders: {assetid}, {assetnumber}, {serialnumber}, {name}, {pluginid}.
qr_target_computer (empty) Custom URL template for computer labels. Blank = link to the computer page. Same placeholders as machine.
qr_target_network_device (empty) Custom URL template for network-device labels. Blank = link to the device page. Same placeholders as machine.
label_default_style card Default asset-label layout when a label first opens: card (badge with image and identity) or plain (just the code and a caption).
label_default_codetype qr Default asset-label code type when a label first opens: qr or barcode (CODE128).
label_default_encodes_machine assetnumber What a machine label encodes by default.
label_default_encodes_computer assetpage What a computer label encodes by default.
label_default_encodes_printer assetpage What a printer label encodes by default.
label_default_encodes_network_device assetpage What a network-device label encodes by default.

Values for those four label_default_encodes_* keys: assetpage, assetnumber, serialnumber, or custom (the matching qr_target_<type> template). All of it is overridable on the label page itself.

There is no measuring_tool label type on this branch. Metrology instruments are machines (ADR-018), so use the machine label keys, and the measuring-tools plugin is not bundled here - shopdb/core/api/settings.py declares four qr_target_* and four label_default_encodes_* keys to match.

Route What it does
/print/asset-label/<assettype>/<id> One label. Public, like the other /print/* pages. <assettype> is machine, computer, printer or network_device; <id> is the asset's plugin id.
/print/asset-label-batch/<assettype> A multi-selection of one type onto ULINE sheets: 6-up 3 in x 3 in, or a dense 72-up mini-label, with a start-cell offset for reusing partial sheets. Reached from the "Print Labels" button on each asset list page.

map

SUPERSEDED as of 0.11.0. Editing these four keys changes nothing on the map. A blueprint and its pixel dimensions are properties of a LEVEL now (ADR-017). They are retained only so a downgrade finds the blueprint it had; the levels migration copied their values onto the first level. Manage plans in Settings > Buildings and levels - see FLOOR-MAP.md.

Key Default Superseded by
map_blueprint_light /static/images/floorplan-placeholder.svg maplevels.blueprintlight
map_blueprint_dark /static/images/floorplan-placeholder.svg maplevels.blueprintdark
map_width 3300 maplevels.mapwidth
map_height 2550 maplevels.mapheight

integrations

Key Default Notes
servicenow_enabled true Enable ServiceNow ticket recognition and links. Disabled = tickets render as plain text.
servicenow_search_url geaerospaceqa.service-now.com global-search template {ticket} is substituted.
servicenow_ticket_prefixes GEINC,GECHG,GERIT,GESCT Comma-separated prefixes recognized as ServiceNow tickets.
servicenow_incident_url geaerospaceqa.service-now.com global-search template {ticket} is substituted. Replace with a direct incident URL if your instance has one.
servicenow_change_url geaerospaceqa.service-now.com global-search template {ticket} is substituted. Replace with a direct change URL if your instance has one.
zabbix_enabled false Enable Zabbix for printer supply monitoring.
zabbix_url (empty) Zabbix API URL.
zabbix_token (empty) Zabbix API token (masked).
warranty_dell_enabled false Enable Dell warranty (service-tag) lookups.
warranty_dell_clientid (empty) Dell TechDirect API client id.
warranty_dell_clientsecret (empty) Dell TechDirect API client secret (masked).
warranty_dell_tokenurl (empty) Dell OAuth token URL. Blank = Dell default.
warranty_dell_apiurl (empty) Dell warranty API URL. Blank = Dell default.

email

Key Default Notes
smtp_enabled false Enable email notifications and alerts.
smtp_host (empty) SMTP server hostname.
smtp_port 587 SMTP port (587 TLS, 465 SSL, 25 plain).
smtp_username (empty) SMTP auth username.
smtp_password (empty) SMTP auth password (masked).
smtp_use_tls true Use TLS for the SMTP connection.
smtp_from_address (empty) From address for outgoing email.
smtp_from_name ShopDB From name for outgoing email.
alert_recipients (empty) Default alert/report recipients (comma-separated).

The mailer (shopdb/utils/mailer.py) reads these settings-first, falling back to matching SMTP_* environment variables only when at least one of them is set. When smtp_enabled is false or smtp_host is blank, every send is a graceful no-op that logs a warning, so an unconfigured site never crashes. The password is never logged.

Three flows send mail, and there is no scheduler in this app - sending is always on-demand:

  • Welcome email, on POST /api/users. Best-effort: the account is created even if the send fails (the response carries a warning). The new account is flagged mustchangepassword, so the frontend forces /change-password before the app; changing it clears the flag and resets lockout counters. Opt out with sendwelcome: false in the create body.
  • Test email, POST /api/settings/test-email (settings.edit), behind the "Send Test Email" button. A real SMTP error is surfaced with the password scrubbed out.
  • Alerts and report delivery, POST /api/reports/email (reports.export), taking {subject, columns, rows, intro?, to?} and mailing the rows as an HTML table. Recipients default to alert_recipients. To automate a recurring send, point an external cron job at this endpoint with a PAT scoped to reports.export.

audit

Retention is audit_retentiondays, in the site section. There is only one such key; anything writing audit_retention_days is writing to nothing.

auth

SAML is not implemented yet. These keys exist so a site can be configured ahead of the feature, and the settings page says so on screen. Nothing reads them: there is no SAML login route and no assertion is accepted, so sign-in is local accounts only whatever is set here. In particular saml_allow_local_login does NOT disable password login - the toggle is locked on until there is another way in, because a control that appears to close the local door while it stays open is worse than no control.

Key Default Notes
saml_enabled false Reserved. Enable SAML SSO once it ships.
saml_idp_metadata_url (empty) Reserved. SAML IdP metadata URL.
saml_entity_id (empty) Reserved. SAML SP entity id.
saml_acs_url (empty) Reserved. SAML Assertion Consumer Service URL.
saml_allow_local_login true Reserved, and locked on. Local login cannot be turned off while it is the only way in.
saml_auto_create_users true Reserved. Auto-create users on first SAML login.
saml_admin_group (empty) Reserved. SAML group name that grants the admin role.

Personal API tokens (PATs) have no configuration here, and JWT_ACCESS_TOKEN_EXPIRES does not apply to them. They are created and revoked from Settings > API Tokens; the token model is documented once, in IMPORT-API.md.

tools

Key Default Notes
securesend_allow_guests false Let people with no account create Secure Send links. Public (readable before login) so the create page can show a guest the form instead of a sign-in wall. Off by default: a guest secret has no creator, so the audit trail can say when and from where but never who. Receiving a secret never needs an account either way.

SECURE-SEND.md is authoritative for the rest of it - guest rate limiting, why the read fails closed, and why the size limit is not a tuning knob.

identifiers (dynamic)

One boolean key per asset identifier per asset type, keyed identifier_<name>_<assettype>_enabled (default true). Admins choose which optional identifiers show on which asset types. See ADR-001. The exact set is generated from IDENTIFIER_LABELS x IDENTIFIER_ASSETTYPES in shopdb/core/api/settings.py.

search (dynamic)

One boolean key per search domain, keyed search_<type>_enabled (default true). Toggles whether a domain appears in global search results. The set is generated from SEARCH_DOMAINS in shopdb/core/api/settings.py.

Search terms are matched word-wise: a multi-word query returns rows containing EVERY word, each word anywhere in the searched fields, in any order ("CSF Roles" matches a row with "CSF" and "Roles" in different columns). Quoting does not force a contiguous phrase.

Custom fields

Site-defined extra attributes per asset type (Settings > Custom Fields, table customfields). Each field has a searchable flag (default off). When on, the field's stored values are matched by global search and a hit routes to the owning asset's detail page. The asset's search_<type>_enabled domain toggle still applies, so a custom-field hit on a computer only shows when the computer search domain is enabled. Inactive or non-searchable fields are never matched.


See also

  • DEPLOY.md - per-site deployment runbook
  • UPGRADE.md - upgrading an existing site
  • BACKUP-RESTORE.md - backup and restore
  • GET /api/docs - what endpoints exist and their auth, generated from the code