A simple Flask app that visualizes incident-free days across a year. It renders a year-long calendar that colors each day based on whether an incident was reported and provides a form to add new incidents.
- Year view calendar with green (incident-free), red (incident), and grey (future/unknown) days.
- Tabbed interface to switch between the calendar and the incident submission form.
- Incident list showing everything recorded in
incidents.json, sorted newest first. - Accepts incident dates as either
YYYY-MM-DD(native date input) orMM/DD/YYYY.
-
Install dependencies (Flask, Pillow, and related libraries)
sudo apt-get update && sudo apt-get install -y fonts-dejavu-core python -m venv .venv source .venv/bin/activate pip install -r requirements.txt
-
Run the app
python app.py
The server listens on
http://0.0.0.0:8080/.
-
Create a dedicated user (optional but recommended)
sudo useradd --system --shell /usr/sbin/nologin --home /opt/incident-free-days incident sudo mkdir -p /opt/incident-free-days sudo chown -R incident:incident /opt/incident-free-days
-
Copy the app to the service directory
sudo cp -r /workspace/incident-free-days /opt/incident-free-days/app sudo chown -R incident:incident /opt/incident-free-days/app
-
Create the virtual environment and install dependencies
cd /opt/incident-free-days/app python -m venv .venv source .venv/bin/activate pip install -r requirements.txt deactivate
-
Create the systemd unit file
sudo tee /etc/systemd/system/incident-free-days.service > /dev/null <<'EOF' [Unit] Description=Incident Free Days Flask app After=network.target [Service] Type=simple User=incident WorkingDirectory=/opt/incident-free-days/app ExecStart=/opt/incident-free-days/app/.venv/bin/python app.py Restart=on-failure Environment=FLASK_ENV=production [Install] WantedBy=multi-user.target EOF
-
Start and enable the service
sudo systemctl daemon-reload sudo systemctl start incident-free-days.service sudo systemctl status incident-free-days.service sudo systemctl enable incident-free-days.serviceThe app will now start on boot and can be managed via
systemctl. -
Report an incident
- Open the app in a browser and switch to the Configuration tab.
- Incidents are stored in
incidents.jsonin the project root. The file is created on first save. - Each incident is a JSON object with keys
inc_number,date(YYYY-MM-DD),severity, andpillar. - Days are colored red when one or more incidents exist for that date; past days without incidents are green.
- The default year rendered on the calendar is controlled by
DEFAULT_YEARinapp.py(currently2025). - The calendar uses Sunday as the first day of the week (
calendar.Calendar(firstweekday=6)). - To sync incidents from incident.io, set
INCIDENT_IO_API_TOKEN(and optionallyINCIDENT_IO_BASE_URLif you use a non-default host). - To configure an ESP32-driven OSHA display, visit the hidden
/dpostroute and provide the display IP/port plus automation preferences.
The app can pull incidents directly from incident.io so you do not need to manually upload CSV files.
- Open the app and go to Configuration.
- Enter your
INCIDENT_IO_API_TOKENand optional base URL override. Tokens are stored locally insync_config.jsonand are never displayed back in the UI. - Pick a sync cadence (once an hour, once a day [default], or once a week) and optionally set a start/end date window.
- Click Dry run mapping to preview the first 10 normalized payloads and verify pillars/severities.
- Click Import incidents to pull all incidents in the selected window into
incidents.json/others.json.
- The Flask server now runs a lightweight background worker that honors your selected cadence as long as the app process is running. It will trigger
/sync/incidentswith the stored token/base URL/date window and update the Last sync status on completion. - To keep the worker alive between reboots, run the app under a supervisor such as the systemd unit described above; you don't need an extra cron entry.
- If you prefer to offload scheduling, you can alternatively point an external scheduler (cron, systemd timer, Kubernetes CronJob) at
POST http://<host>:8080/sync/incidentswith the token/base URL payload. In that mode, set cadence to "off" or ignore the in-app cadence.
-
Export your incident.io API token to the environment:
export INCIDENT_IO_API_TOKEN="<token>" # Optional: override the API host # export INCIDENT_IO_BASE_URL="https://eu.api.incident.io"
-
Run a dry run from the feature branch to verify parsing and mapping without writing to disk:
python app.py --sync-incidents --dry-run
-
Sync data to
incidents.json/others.jsononce the output looks correct:python app.py --sync-incidents
You can also trigger the sync over HTTP using GET /sync/incidents?dry_run=1 (dry run) or POST /sync/incidents with a JSON payload (token, optional date range) to persist results.
- Visit
/dpostto enter the display IP (e.g.,192.168.1.25or192.168.1.25:80) and enable pushing. Settings are stored locally alongside the incident.io token. - When enabled, the OSHA tab shows a Send to display button that rerenders the OSHA sign and pushes it over the ESP32
start/chunk/endprotocol. - Automation options:
- Daily push at a configured local time (defaults to
05:00 America/Phoenix). A background worker in the Flask process handles the schedule; keep the service running (e.g., via systemd) to preserve the cadence. - Push on new procedural incident: after each incident.io sync, the app detects a new procedural RCA incident and sends the updated sign once per incident number.
- Daily push at a configured local time (defaults to
- The OSHA status panel records the last push time, incident number, and any error text from the ESP32.
- Verify the IP/port in
/dpostand that the ESP32 is reachable over HTTP. - Timeouts or
display request failederrors usually indicate network issues or a busy display; retry after a few seconds. - If pushes do not run daily, confirm the server process is running (or managed by systemd) so the background scheduler can fire.
app.py— Flask application entrypoint and routing.templates/index.html— Calendar layout, tabs, and incident table.static/styles.css— Styling for the calendar, tabs, and forms.static/tabs.js— Simple tab switcher for the UI and HTML-to-image export logic.
- Upload a product → pillar key JSON file using the Upload product → pillar key form; the key is saved alongside
incidents.json. - Duplicate incident numbers are skipped when adding incidents via sync.
- The calendar tab includes an Export as Image button that captures the visible calendar into a PNG.
- The export runs completely in the browser (no external CDN dependency), so it works even in offline environments.
- The server also provides
/calendar/eink.png, which returns a 1600×1200 PNG calendar for the current month by default. You can override the period with?year=YYYY&month=MM. This endpoint is designed for consumption by external clients such as a Raspberry Pi driving a 13.3" e-ink display.