315 lines
9.6 KiB
Markdown
315 lines
9.6 KiB
Markdown
# 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
|
|
|
|
```sh
|
|
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:
|
|
|
|
```yaml
|
|
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`:
|
|
|
|
```yaml
|
|
flask:
|
|
url_prefix: "/fatima"
|
|
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:
|
|
|
|
```sh
|
|
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.
|
|
|
|
```sh
|
|
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:
|
|
|
|
```sh
|
|
python3 -c "import secrets; print(secrets.token_hex(32))"
|
|
```
|
|
|
|
Create `/etc/fatima/config.yaml` using `config.example.yaml` as a guide:
|
|
|
|
```sh
|
|
sudo install -d -o root -g fatima -m 750 /etc/fatima
|
|
```
|
|
|
|
```yaml
|
|
flask:
|
|
secret_key: "paste-the-generated-random-value-here"
|
|
url_prefix: "/fatima"
|
|
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:
|
|
|
|
```sh
|
|
sudo chown root:fatima /etc/fatima/config.yaml
|
|
sudo chmod 640 /etc/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`:
|
|
|
|
```ini
|
|
[Unit]
|
|
Description=Fatima calendar
|
|
After=network.target
|
|
|
|
[Service]
|
|
Type=simple
|
|
User=fatima
|
|
Group=www-data
|
|
SupplementaryGroups=fatima
|
|
WorkingDirectory=/srv/fatima
|
|
Environment=APP_CONFIG=/etc/fatima/config.yaml
|
|
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:
|
|
|
|
```sh
|
|
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:
|
|
|
|
```sh
|
|
sudo -u www-data curl --unix-socket /run/fatima/gunicorn.sock \
|
|
--head http://localhost/fatima/
|
|
```
|
|
|
|
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`:
|
|
|
|
```nginx
|
|
upstream fatima_gunicorn {
|
|
server unix:/run/fatima/gunicorn.sock;
|
|
}
|
|
|
|
server {
|
|
listen 80;
|
|
listen [::]:80;
|
|
server_name calendar.example.com;
|
|
|
|
location = /fatima {
|
|
return 301 /fatima/;
|
|
}
|
|
|
|
location /fatima/ {
|
|
proxy_pass http://fatima_gunicorn;
|
|
proxy_http_version 1.1;
|
|
|
|
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;
|
|
}
|
|
}
|
|
```
|
|
|
|
There is deliberately no URI after `proxy_pass`: Nginx must forward
|
|
`/fatima/...` unchanged because Fatima's prefix middleware removes that prefix
|
|
before Flask routes the request. Do not rewrite it to `/`.
|
|
|
|
Enable and validate the site:
|
|
|
|
```sh
|
|
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/fatima/
|
|
```
|
|
|
|
Add TLS before exposing the login publicly. Once the certificate is configured,
|
|
redirect HTTP to HTTPS and keep the same `/fatima/` proxy block in the TLS
|
|
server. 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:
|
|
|
|
```sh
|
|
sudo -u fatima /srv/fatima/.venv/bin/pip install -r /srv/fatima/requirements.txt
|
|
sudo systemctl restart fatima
|
|
sudo systemctl reload nginx
|
|
```
|
|
|
|
Useful checks:
|
|
|
|
```sh
|
|
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:
|
|
|
|
```sh
|
|
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:
|
|
|
|
```sh
|
|
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:
|
|
|
|
```sh
|
|
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`.
|