2026-07-17 19:43:19 +02:00
2026-07-17 19:43:19 +02:00
2026-07-18 22:32:56 +02:00
2026-07-18 22:32:56 +02:00
2026-07-18 22:32:56 +02:00
2026-07-17 20:02:11 +02:00
2026-07-26 11:17:19 +02:00

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/Fatima for the application and virtual environment
  • /etc/Fatima/config.yaml for secrets and application configuration
  • /var/lib/fatima/calendar.sqlite for persistent data
  • /run/fatima/gunicorn.sock for the private Gunicorn socket
  • fatima.service to run the application as an unprivileged fatima user

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.

S
Description
Perpetual payments calendar
Readme
178 KiB
Languages
Python 55.8%
JavaScript 17%
CSS 16.6%
HTML 10.6%