Skip to content

leobaray/epi_system

Repository files navigation

EPI System

Python License Runtime deps

Web application for managing personal protective equipment (PPE, EPI in Portuguese) in Brazilian workplaces: product catalogue, stock, assignment to employees, signed PDF receipts, and the compliance trail required by the NR-6 regulation and the eSocial S-2240 event.

The problem

A Brazilian employer must be able to prove, per employee and per item, which PPE was handed out, when, under which Certificado de Aprovação (CA) number, and when that CA expires. Most small operations track this in spreadsheets, which fall apart precisely when a labour inspection asks for the signed receipt. This system keeps the whole chain — product → stock movement → assignment → signed receipt → periodic inspection checklist — in one auditable place, and it derives the pending work (expiring CAs, low stock, overdue replacements, missing checklists) automatically.

It is built deliberately on the Python standard library: http.server for the HTTP layer and sqlite3 for storage. No web framework, no ORM, no message broker. The only runtime dependency is ReportLab, and only for generating PDF receipts. It starts from a single python run.py on any machine with Python 3.12.

Screenshots

Dashboard Register assignment
Dashboard Assign PPE
Products & stock Kits
Products Kits

Features

Catalogue, stock and kits

  • Product catalogue with manufacturer, cost, CA number and CA expiry date, sizes, lot, and INMETRO fields.
  • Per-product stock with balance, low-stock and out-of-stock detection.
  • Kits — named bundles of products handed out as a single withdrawal (services/kits.py).
  • Risk matrix mapping a job role to the PPE it requires; the dashboard surfaces employees missing required items (services/matrix.py).

Assignment and lifecycle

  • Individual and multi-item (kit) withdrawals, each deducting stock in one transaction.
  • Soft-delete of a withdrawal restores the stock it consumed and writes an audit record.
  • Replacement prediction: from each product's post-delivery useful life (lifespan_days) and the last delivery per employee, the system computes when a swap is due and flags overdue / due-soon items (services/lifecycle.py).
  • PDF delivery receipt for signature, with a per-document SHA-256 for tamper-evidence and the NR-6 / S-2240 citation (pdf/receipts.py, via ReportLab).
  • Employee registry with CPF validation, including the two check digits (services/employees.py, tests/test_cpf.py).

Compliance

  • Periodic inspection checklists on a 90-day cycle with overdue detection: is_checklist_overdue() / get_checklist_status() report how many days remain or how far past due the next inspection is (services/compliance.py).
  • PDF checklist upload, validated by magic bytes and size, stored under checklists_uploads/.
  • Audit log of destructive operations (deleted withdrawals, products, kits) with an item snapshot (services/audit.py).

Security (each item is exercised by the test suite)

  • Passwords hashed with PBKDF2-HMAC-SHA256, 200k iterations, per-user random salt, compared with secrets.compare_digest. On a login for a non-existent user a dummy hash is still verified, so the failed path costs the same time and cannot be used to enumerate accounts (core/auth.py, tests/test_login_timing_m06.py).
  • Login lockout with exponential backoff per (IP, username) — a 30 s base doubling to a 15 min ceiling, both tunable via EPI_LOGIN_LOCKOUT_BASE / EPI_LOGIN_LOCKOUT_MAX (tests/test_login_ratelimit_b05.py).
  • CSRF double-submit token on every mutating form; session and CSRF cookies are HttpOnly; SameSite=Strict (tests/test_csrf_multi_form.py, tests/test_csrf_cookie_httponly.py).
  • Role-based access: require_auth / require_admin gate destructive routes deny-by-default (tests/test_route_security_rbac.py).
  • Strict Content-Security-Policy with a per-request nonce, plus HSTS, X-Frame-Options: DENY, Permissions-Policy and X-Content-Type-Options: nosniff on every response (core/responses.py).
  • Path-traversal guards on every file-serving route; PDF uploads are rejected unless they start with %PDF-; backups are written 0600 in a 0700 directory (tests/test_backup_acl_b06.py).
  • Output escaping and JSON-in-HTML hardening against stored XSS (tests/test_xss_c01.py, tests/test_xss_c02.py).
  • CPFs are masked (111.***.***-35) by a logging filter before any record leaves the process — LGPD hygiene for logs (core/log_filters.py).
  • ruff runs with the S (bandit) security ruleset enabled (pyproject.toml).

Operations

  • A background daemon thread purges expired sessions and takes a consistent snapshot of the database via SQLite's online backup API, with retention of the N most recent copies — configurable by env, no cron required (core/maintenance.py).

Frontend

  • Server-rendered HTML, no build step, no JavaScript framework.
  • Inter and FontAwesome are self-hosted under static/; the app makes no external requests and works fully offline.
  • Keyboard command palette (⌘K), g-prefixed navigation shortcuts, light/dark theme, and accessibility exercised by tests: modal focus trapping, focus restoration, form labelling and search delegation (tests/test_a11y_*.py).

Stack

Layer Choice
Runtime Python ≥ 3.12
HTTP http.server.ThreadingHTTPServer (standard library)
Storage SQLite via sqlite3, WAL mode, one connection per thread
PDF ReportLab ≥ 4.2 — the only runtime dependency
Lint / types ruff (E, F, I, B, UP, SIM, C4, PIE, RET, S), mypy
Tests pytest — 222 tests across 32 modules

Architecture

Three layers split by domain (products, stock, assignments, employees, kits, matrix, compliance, audit) rather than by technical role. app.py is a thin router; services/ holds business rules and owns every SQL statement; views/ renders HTML; core/ provides cross-cutting infrastructure.

run.py                 entry point — python run.py [port], default 8140
  └── app.py           routing, request dispatch, multipart parsing (ThreadingHTTPServer)
        ├── views/     HTML rendering, one module per domain
        ├── services/  business rules + all SQL, one module per domain
        ├── core/      auth (PBKDF2, CSRF, lockout), db, responses/CSP,
        │              log masking, background maintenance
        └── pdf/       signed-receipt generation (ReportLab)

Request flow:

flowchart LR
    C[Browser] -->|GET / POST| A[app.py router]
    A --> AU{require_auth / CSRF / require_admin}
    AU -->|denied| R[302 login / 401 / 403]
    AU -->|allowed| S[services/*]
    S --> DB[(SQLite - thread-local conn, WAL)]
    S --> V[views/* - HTML]
    S --> P[pdf/receipts.py]
    V --> RESP[core/responses.py<br/>CSP nonce + security headers]
    RESP --> C
Loading

Two engineering decisions worth calling out:

  • One SQLite connection per thread (core/db.py, kept in threading.local). A single shared connection with check_same_thread=False under ThreadingHTTPServer lets two threads enter with conn: at once and silently corrupts transactions; the per-thread connection avoids it.
  • A custom multipart parser (app.py, built on email.parser) replaces cgi.FieldStorage, which was removed in Python 3.13 — so file uploads keep working on current interpreters without adding a dependency.

Deliveries made individually (assignments) and via kit batches (withdrawal_items) are unified by a SQL view v_deliveries, so the employee PPE card and the replacement forecast read from one source without merging the two write paths.

Getting started

Requires Python 3.12 or newer.

git clone https://github.com/leobaray/epi_system.git
cd epi_system

python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt        # ReportLab, for PDF receipts

Quick demo — load a realistic dataset (23 employees, 21 products, 6 kits, several hundred back-dated deliveries) and a dev admin whose random password is printed once:

python seed.py       # WARNING: wipes operational tables, then reseeds demo data
python run.py        # serves on http://localhost:8140

From an empty database — set the bootstrap admin via env and skip the seed:

export EPI_ADMIN_USER=admin
export EPI_ADMIN_PASS='choose-a-strong-password'
python run.py        # creates the admin on first start if the users table is empty

run.py accepts an optional port: python run.py 9000. Authentication can be disabled for local development with EPI_AUTH_ENABLED=0.

Development extras and the test suite:

pip install -e ".[dev]"
pytest
ruff check .
mypy .

Selected environment variables

Variable Default Purpose
EPI_AUTH_ENABLED 1 Toggle authentication
EPI_SESSION_TTL 28800 Session lifetime (seconds)
EPI_LOGIN_MAX_FAILURES 5 Failures before lockout
EPI_LOGIN_LOCKOUT_BASE / EPI_LOGIN_LOCKOUT_MAX 30 / 900 Backoff base / ceiling (seconds)
EPI_BACKUP_ENABLED / EPI_BACKUP_KEEP 1 / 7 Automatic backups and retention
EPI_COOKIE_SECURE 0 Force the Secure cookie flag (auto when behind HTTPS)

Project structure

app.py               HTTP handler, routing, multipart upload parsing
run.py               entry point
seed.py              demo-data loader + bootstrap admin (dev utility)
core/                auth, database, HTTP responses/CSP, log masking, maintenance
services/            business logic + SQL, one module per domain
views/               HTML rendering, one module per domain
pdf/                 signed-receipt generation
static/              CSS, self-hosted Inter + FontAwesome
tests/               32 modules — domain, security and accessibility
screenshots/         interface captures used in this README
checklists_uploads/  inspection-checklist template + uploads

Status and limitations

Version 0.2.0. It is a functional, single-tenant application built for a real small-industry use case in Brazil, and it is honest about what it is not:

  • Single tenant: one company per database. There is no multi-tenancy.
  • No built-in TLS: it terminates plain HTTP and binds 0.0.0.0 on purpose (LAN access from plant machines). Put it behind a reverse proxy with TLS if it is exposed beyond a trusted network — the Secure cookie flag and HSTS are already wired for that case.
  • CHECK constraints and ON DELETE rules apply to new databases only (SQLite cannot add them via ALTER TABLE); the service layer enforces the same invariants, and DB constraints are defence-in-depth.
  • The Content-Security-Policy still allows script-src-attr 'unsafe-inline' for legacy onclick handlers; migrating those to full event delegation is a known follow-up.

License

Released under the MIT License.

About

Web app for managing PPE (EPI) in Brazilian workplaces — product/stock catalogue, employee assignments, signed PDF receipts, and the NR-6 / eSocial S-2240 compliance trail. Built on the Python standard library (http.server + sqlite3), no web framework.

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages