Fatimas Calendar
A mobile-first Flask calendar with session authentication and infinite week scrolling.
Amounts and worked/vacation/sick statuses selected for dates are stored persistently in instance/calendar.sqlite, which is created automatically on first launch.
Run it
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
waitress-serve --host=0.0.0.0 --port=9009 flask_fatima:app
To use a different port, substitute it in the command (for example,
--port=8080). The application exposes its WSGI callable as flask_fatima:app, so it
can also be used by platforms that ask for an application entry point.
Set the username, password, and Flask signing key in config.yaml before starting. This file is excluded from Git. Use config.example.yaml as the documented template when setting up another machine.
The two paid quick-choice buttons are configured in the same file:
amounts:
presets:
- "55"
- "27.50"
The €0 and custom-amount controls remain available automatically.
Reverse proxy path
The app is served below /fatima by default. Configure the external path in
config.yaml:
flask:
url_prefix: "" # no prefix if site sits under fatima.suy.nl
log_file: "/tmp/fatima.log"
database: "instance/calendar.sqlite"
Set URL_PREFIX in the environment to override this value at deployment time.
Use an empty value (url_prefix: "" or URL_PREFIX=) to serve from / instead.
The reverse proxy must preserve the prefix when forwarding requests to the WSGI
server (Waitress or Gunicorn).
Production deployment: Nginx → Gunicorn → Fatima
The following example targets a Debian/Ubuntu server using:
/srv/Fatimafor the application and virtual environment/etc/Fatima/config.yamlfor secrets and application configuration/var/lib/fatima/calendar.sqlitefor persistent data/run/fatima/gunicorn.sockfor the private Gunicorn socketfatima.serviceto run the application as an unprivilegedfatimauser
Replace calendar.example.com and the sample credentials before exposing the
site. The examples assume the application remains mounted at /fatima.
1. Install the application
Install the operating-system packages and create a service account:
sudo apt update
sudo apt install nginx python3 python3-venv
sudo useradd --system --user-group --home-dir /srv/Fatima --shell /usr/sbin/nologin fatima
Copy or clone the repository into /srv/Fatima, then create the virtual
environment and install the dependencies. Gunicorn is an additional production
dependency; Waitress remains suitable for the local command documented above.
sudo chown -R fatima:fatima /srv/Fatima
sudo -u fatima python3 -m venv /srv/Fatima/.venv
sudo -u fatima /srv/Fatima/.venv/bin/pip install -r /srv/Fatima/requirements.txt gunicorn
For repeatable deployments, pin the Gunicorn version used by the server in your deployment tooling or requirements file.
2. Create the production configuration
Generate a signing key:
python3 -c "import secrets; print(secrets.token_hex(32))"
flask:
secret_key: "paste-the-generated-random-value-here"
url_prefix: ""
log_file: "/var/log/fatima/startup.log"
database: "/var/lib/fatima/calendar.sqlite"
auth:
username: "replace-with-the-login-name"
password: "replace-with-a-strong-password"
amounts:
presets:
- "55"
- "27.50"
Protect the file because it contains the login password and Flask signing key:
sudo chown root:fatima /srv/Fatima/config.yaml
sudo chmod 640 /srv/Fatima/config.yaml
The systemd unit below creates /var/lib/fatima, /var/log/fatima, and
/run/fatima with ownership suitable for the service. If an existing database
is copied into /var/lib/fatima, make sure it and its parent directory are
writable by fatima; SQLite may create journal files beside the database.
3. Run Gunicorn with systemd
Create /etc/systemd/system/fatima.service:
[Unit]
Description=Fatima calendar
After=network.target
[Service]
Type=simple
User=fatima
Group=www-data
SupplementaryGroups=fatima
WorkingDirectory=/srv/Fatima
ExecStart=/srv/Fatima/.venv/bin/gunicorn \
--workers 2 \
--bind unix:/run/fatima/gunicorn.sock \
--umask 007 \
--access-logfile - \
--error-logfile - \
flask_fatima:app
Restart=on-failure
RestartSec=5
RuntimeDirectory=fatima
RuntimeDirectoryMode=0750
StateDirectory=fatima
StateDirectoryMode=0750
LogsDirectory=fatima
LogsDirectoryMode=0750
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
[Install]
WantedBy=multi-user.target
Group=www-data and --umask 007 allow Nginx to connect to the Unix socket
without exposing it to other users. APP_CONFIG makes the application load the
production YAML file rather than /srv/fatima/config.yaml.
Load and start the service:
sudo systemctl daemon-reload
sudo systemctl enable --now fatima
sudo systemctl status fatima
sudo journalctl -u fatima -n 50 --no-pager
The startup log should show the resolved production database path. Before configuring Nginx, verify Gunicorn directly:
sudo -u www-data curl --unix-socket /run/fatima/gunicorn.sock \
--head http://localhost
A redirect to /fatima/login is the expected response when no authenticated
session exists.
4. Proxy /fatima through Nginx
Create /etc/nginx/sites-available/fatima:
upstream fatima_gunicorn {
server unix:/run/fatima/gunicorn.sock;
}
server {
listen 80;
listen [::]:80;
server_name calendar.example.com;
location / {
proxy_pass http://unix:/run/fatima/gunicorn.sock;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 60s;
}
}
sudo ln -s /etc/nginx/sites-available/fatima /etc/nginx/sites-enabled/fatima
sudo nginx -t
sudo systemctl reload nginx
curl --head http://calendar.example.com/
The application stores authentication in a signed browser session, so
keep secret_key stable across restarts and deployments; changing it logs out
all existing sessions.
Updating and troubleshooting
After deploying new source or dependencies:
sudo -u fatima /srv/Fatima/.venv/bin/pip install -r /srv/Fatima/requirements.txt
sudo systemctl restart fatima
sudo systemctl reload nginx
Useful checks:
sudo journalctl -u fatima -f
sudo tail -f /var/log/nginx/error.log
sudo nginx -t
sudo systemctl status fatima nginx
If Nginx returns 502 Bad Gateway, confirm that fatima.service is running and
that www-data can access /run/fatima/gunicorn.sock. If the app starts but
cannot save calendar entries, check ownership of /var/lib/fatima and confirm
the startup summary names /var/lib/fatima/calendar.sqlite.
Import the Excel calendar
Preview an import without changing the database:
python scripts/import_excel.py /path/to/fatima.xlsx
The preview prints the absolute workbook and database paths, current database row counts and date range, discovered workbook values, and overwrite counts. Check that the printed database path is the same file used by the production app before applying the import.
Add --apply to import:
python scripts/import_excel.py /path/to/fatima.xlsx --apply
The importer refuses to write if the database is missing, has the wrong schema, or the workbook contains no importable data. It creates a consistent SQLite backup before writing, reads every imported value back after committing, and prints the final row counts and date range. If production uses a non-default database location, specify it explicitly:
python scripts/import_excel.py /path/to/fatima.xlsx --database /absolute/path/calendar.sqlite --apply
On the iPhone, visit http://<your-computer-on-the-local-network>:9009/fatima/.
Waitress listens on all network interfaces with the command above. For anything
exposed beyond a trusted home network, put it behind an HTTPS reverse proxy.
For deployments, APP_CONFIG can point to a YAML file elsewhere. SECRET_KEY,
APP_USERNAME, APP_PASSWORD, DATABASE, and LOG_FILE environment variables optionally
override the corresponding values. DATABASE should be an absolute path to the
SQLite file used in production.
The YAML flask.database path is resolved relative to the directory containing
the selected config file. If it is omitted, the app uses
instance/calendar.sqlite beside flask_fatima.py; it no longer relies on
Flask's environment-dependent inferred instance directory.
At startup, the app prints a database summary to the Waitress log containing the
resolved path, amount and status totals, distinct recorded-date total, and date
range. Compare that path and those totals with the importer's output to confirm
that both processes use the same database. The timestamped summary is also
appended to the configured flask.log_file each time the app starts. It defaults
to /tmp/fatima.log and can be overridden at deployment time with LOG_FILE.