Settings
Settings
Settings — Technical Spec
app/im2.py, template app/templates/settings.html.
Routes
| Method | Path | Auth | Notes |
|---|---|---|---|
| GET | /settings | role == 'admin' (403 "Admins only") | Renders scalar settings (left card) and LIST_SETTINGS textareas (right cards; gen_locs gets its own card). |
| POST | /api/settings | admin | Whole-form save. Returns {ok:true, changed:N}. |
Storage and resolution
- Table
app_settings(key pk, value, updated_by, updated_at). settings(conn=None)builds a dict fromSETTING_DEFSdefaults +HIDDEN_SETTINGS, then overlays non-NULLapp_settingsrows. Table beats code; code default is the fallback. No caching — every call opens a connection unless one is passed.- Helpers:
setting_bool(key)(true only for exactlyyes, case-insensitive, trimmed),setting_list(value)(newline list, case-insensitive de-dupe, alphabetical),setting_uoms(vals)(commas also parsed, upper-cased, falls back to codeUOMS),setting_classes(vals)→[{"name","id"}]parsed fromName = qb_class_idlines. BOOL_SETTINGS=allow_fp_watcher,allow_auto_po,wb_count_before_release,rush_notify_slack,rush_notify_sms,rush_notify_tech_sms.LIST_SETTINGS=uoms(no column),item_groups→item_group,gen_locs→gen_loc,count_groups→count_group,ro_classes(no column). Keys with a column and no saved row are pre-filled fromlist_values(conn, key, column)(in-use values).HIDDEN_SETTINGS=qb_income_account_id,qb_expense_account_id,qb_asset_account_id, defaulting toqb.INCOME_ACCOUNT_ID/EXPENSE_ACCOUNT_ID/ASSET_ACCOUNT_ID(714/638/815, env-overridable in/opt/im2/env). Not rendered, not saveable here.
Save semantics (POST /api/settings)
Per key in SETTING_KEYS, skipped entirely if absent from the form (so a partial form only touches what it posts):
- trim; list keys re-serialized through
setting_list; bool keys coerced toYes/No;uomsupper-cased;default_lead_time_daysmust parse as a number → stored asint, else 400 "Default lead time must be a whole number of days". - Unchanged vs
settings(conn)→ skipped, not counted. val == ""→delete from app_settings where key = %s(revert to code default; an empty override is never stored).- Otherwise upsert with
updated_by,updated_at = now(). - One
audit_logrow per change:sku = NULL,field_name = key, old→new,source='settings'.
Consumers
allow_fp_watcher → ro_watch.py; allow_auto_po + wb_count_before_release → po.py workbench/buy list; ro_classes → RO screens and ro_post.py; uoms → grid, new item, RO lines; income_gl/expense_gl + hidden account ids → qb.py new-item push; default_lead_time_days → Purchasing Workbench; rush_notify_* → notify.py.
Known limits / defects
- Account names are free text and never validated against QuickBooks; a typo surfaces later as a rejected item push.
ro_classesids are not validated either — a wrong id posts to the wrong QB class silently.settings()/setting_bool()open a fresh connection per call when noconnis passed (notify.pyand several route paths do exactly that). Cheap per request, but it is a real DB round trip inside best-effort notification code.- No CSRF token; cookie auth +
samesite=laxonly. A single POST can change every setting, including turningallow_auto_poon. scripts/schema.sqlin this package does not containapp_settings; the live DB has it. Replaying that DDL clean would not produce a working Settings screen.updated_at.strftimeis called unguarded in the template — a row with a NULLupdated_at(only reachable by hand-written SQL) would 500 the screen.