Files

401 lines
11 KiB
Markdown
Raw Permalink Normal View History

2026-07-22 20:38:10 +02:00
# L@pp
2026-01-01 19:58:16 +00:00
2026-07-22 20:38:10 +02:00
L@pp is a small, mobile-friendly shared-list application built with Flask. It is
designed primarily for grocery lists, but works just as well for chores, packing
lists, or any other lightweight checklist.
2026-01-01 21:33:12 +01:00
2026-07-22 20:38:10 +02:00
Users can create and share lists, add items with quantities and categories, and
check items off from a phone-friendly interface. The included web app can also
be installed as a Progressive Web App (PWA).
2026-07-22 20:38:10 +02:00
## Features
2026-07-22 20:38:10 +02:00
- Multiple lists per user
- List sharing between registered users
- Item quantities and units, including input such as `Milk 2 cartons`
- Grocery categories with icons
- Reusable suggestions for previously completed items
- Bulk item entry
- Responsive, installable PWA interface
- User registration with group invitation keys
- Administrator approval for new accounts
- Session-based JSON API for companion clients
- SQLite by default, with other SQLAlchemy database URLs supported
2026-07-22 20:38:10 +02:00
## Requirements
2026-07-22 20:38:10 +02:00
- Python 3.9 or newer
- `pip` and Python virtual-environment support
2026-07-22 20:38:10 +02:00
## Quick start
2026-07-22 20:38:10 +02:00
Clone the repository and enter its directory:
2026-07-22 20:38:10 +02:00
```bash
git clone <repository-url>
cd lapp
```
Create a virtual environment and install the dependencies:
```bash
python3 -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install -r requirement.txt
```
Initialize the database with an administrator, a regular user, two groups, and
some example lists:
```bash
python initialize_data.py
```
The command creates `instance/secrets.yaml` and `instance/app.db`. It does not
print the generated passwords or registration keys; open
`instance/secrets.yaml` locally to retrieve them. The seeded usernames are
`admin` and `ignace`.
Start the development server:
```bash
python lapp.py
```
Then open <http://127.0.0.1:5001>. The built-in server runs in debug mode and is
intended for local development only.
Running the application without `initialize_data.py` creates an empty database
schema, but no users or registration groups.
## Configuration and secrets
Private configuration lives in `instance/secrets.yaml`. On first use, missing
values are generated automatically and the file is restricted to mode `0600`.
A generated file has this structure:
```yaml
runtime:
secret_key: <Flask session secret>
application_key: <key used by /api/validate_key>
database_uri: sqlite:///app.db
initial_data:
admin_password: <initial admin password>
admin_group: <initial admin registration key>
user_password: <initial user password>
user_group: <initial user registration key>
fernet:
key: <Fernet key used by test.py>
```
For SQLite, relative database paths are resolved inside Flask's `instance/`
directory. To use another database, set `runtime.database_uri` to a compatible
SQLAlchemy URL and install the corresponding database driver.
Keep `runtime.secret_key` stable. Replacing it invalidates active sessions and
the seven-day auto-login cookie. The `runtime.application_key` is checked by
`POST /api/validate_key`.
The complete `instance/` directory is ignored by Git. Back it up securely and
never commit or share its contents. In particular, back up both `app.db` and
`secrets.yaml` together when using the default SQLite setup.
### Customizing the initial accounts
To choose credentials instead of using generated values:
1. Generate the configuration and empty schema without seeding accounts:
```bash
python -c 'from lapp import create_app; create_app()'
```
2. Edit the values under `initial_data` in `instance/secrets.yaml`.
3. Run `python initialize_data.py`.
Initialization is deliberately idempotent: if the database already contains a
user, no seed data is added. It also refuses to initialize a partially populated
database.
## How accounts and sharing work
Registration requires a group key. The two keys created during initialization
are stored as `initial_data.admin_group` and `initial_data.user_group`. A newly
registered account remains pending until an administrator approves it at
`/admin/`.
After signing in, a user can create, activate, rename, share, or delete their own
lists. A shared list is visible and editable by every selected user. Only the
owner can change the list itself or its sharing settings.
Administrators can create additional registration groups from `/groups`.
## Item input
L@pp extracts simple quantities and units from item text. For example:
| Input | Label | Quantity | Unit |
| --- | --- | ---: | --- |
| `Apples 4` | Apples | 4 | `x` |
| `Milk 2 cartons` | Milk | 2 | `cartons` |
| `3 kg potatoes` | Potatoes | 3 | `kg` |
Cleaning a list moves checked items into its suggestion history. Those items can
then be added again quickly from the add-items screen.
## API overview
API requests use form-encoded fields and return JSON. Authentication is stored
in the Flask session cookie, so API clients must retain cookies after login.
| Method | Endpoint | Purpose |
| --- | --- | --- |
| `POST` | `/api/validate_key` | Validate the configured application key (`key`) |
| `POST` | `/api/login` | Sign in with `user` and `password` |
| `POST` | `/api/register` | Register with `user` and a group key in `password` |
| `POST` | `/api/load_lists` | Load lists available to the signed-in user |
| `POST` | `/api/load_items` | Load active items for `listid` |
Responses use a numeric `status` field: `1` means success, `0` means failure,
and registration uses `2` to indicate that approval is pending.
Example login with a cookie jar:
```bash
curl -c cookies.txt \
-X POST \
-d 'user=ignace' \
-d 'password=YOUR_PASSWORD' \
http://127.0.0.1:5001/api/login
curl -b cookies.txt \
-X POST \
http://127.0.0.1:5001/api/load_lists
```
## Production deployment with Nginx and Gunicorn
The following example targets Debian or Ubuntu, installs L@pp in `/srv/lapp`,
and serves it from `lists.example.com`. Substitute your own installation path
and domain where necessary.
Gunicorn loads the existing Flask application factory as
`lapp:create_app()`. The repository's `lapp.wsgi` file is only needed for a
mod_wsgi deployment and is not used here.
### 1. Install the system packages
```bash
sudo apt update
sudo apt install nginx python3 python3-venv
```
Create a dedicated, unprivileged service account:
```bash
sudo useradd --system \
--home /srv/lapp \
--shell /usr/sbin/nologin \
lapp
```
Place or clone the repository at `/srv/lapp`, then install the application and
Gunicorn:
```bash
cd /srv/lapp
sudo python3 -m venv .venv
sudo .venv/bin/pip install --upgrade pip
sudo .venv/bin/pip install -r requirement.txt
sudo .venv/bin/pip install 'gunicorn>=23,<24'
```
Gunicorn should run as the service account, not as `root`, and should only be
reachable through Nginx.
### 2. Initialize the application
Create the private instance directory with permissions that allow the service
account to maintain the SQLite database:
```bash
sudo install -d \
-o lapp \
-g www-data \
-m 0750 \
/srv/lapp/instance
sudo -u lapp /srv/lapp/.venv/bin/python \
/srv/lapp/initialize_data.py
```
View the generated passwords and registration keys locally:
```bash
sudo -u lapp sed -n '1,120p' \
/srv/lapp/instance/secrets.yaml
```
Back up `instance/app.db` and `instance/secrets.yaml` together. Do not expose
the `instance/` directory through Nginx.
### 3. Run Gunicorn with systemd
Create `/etc/systemd/system/lapp.service`:
```ini
[Unit]
Description=L@pp service
After=network.target
[Service]
Type=simple
User=lapp
Group=www-data
WorkingDirectory=/srv/lapp
RuntimeDirectory=lapp
RuntimeDirectoryMode=0750
UMask=0007
ExecStart=/srv/lapp/.venv/bin/gunicorn \
--workers 2 \
--bind unix:/run/lapp/lapp.sock \
--access-logfile - \
--error-logfile - \
lapp:create_app()
Restart=on-failure
RestartSec=5
PrivateTmp=true
NoNewPrivileges=true
[Install]
WantedBy=multi-user.target
```
One or two workers is a sensible starting point for this small SQLite
application. Adding many workers can increase SQLite write contention.
Enable and start the service:
```bash
sudo systemctl daemon-reload
sudo systemctl enable --now lapp
sudo systemctl status lapp
```
Follow its logs or verify that its socket exists:
```bash
sudo journalctl -u lapp -f
sudo ls -l /run/lapp/lapp.sock
```
### 4. Configure Nginx
Create `/etc/nginx/sites-available/lapp`:
```nginx
server {
listen 80;
listen [::]:80;
server_name lapp.suy.nl;
client_max_body_size 2m;
location /static/ {
alias /srv/lapp/static/;
expires 7d;
add_header Cache-Control "public";
}
location / {
proxy_pass http://unix:/run/lapp/lapp.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;
}
}
```
Enable the site and validate the configuration before reloading Nginx:
```bash
sudo ln -s /etc/nginx/sites-available/lapp \
/etc/nginx/sites-enabled/lapp
sudo nginx -t
sudo systemctl reload nginx
```
The application should now be available at `http://lists.example.com`.
### 5. Enable HTTPS
Once the domain's DNS records point to the server, obtain a TLS certificate
with your preferred ACME client. For example, using Certbot:
```bash
sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d lists.example.com
```
Do not expose L@pp publicly over plain HTTP: it handles passwords and persistent
login cookies.
### Updating a deployment
Back up the database and secrets first. Then update the source and dependencies
and restart Gunicorn:
```bash
cd /srv/lapp
sudo .venv/bin/pip install -r requirement.txt
sudo systemctl restart lapp
sudo systemctl status lapp
```
See the official [Flask Gunicorn deployment guide](https://flask.palletsprojects.com/en/stable/deploying/gunicorn/),
[Gunicorn documentation](https://docs.gunicorn.org/en/stable/run.html), and
[Nginx proxy module documentation](https://nginx.org/en/docs/http/ngx_http_proxy_module.html)
for additional configuration options.
## Utility scripts
- `initialize_data.py` safely seeds a new database.
- `show_db.py` prints all database records for local diagnostics. Its output
includes password hashes and group secrets, so treat it as sensitive.
- `update_db.py` contains historical, manual migration snippets. Review and
adapt it before running it against any database.
- `test.py` is a Fernet encryption example, not an automated test suite.
## Project structure
```text
lapp.py Application factory and blueprint registration
auth.py Login, registration, logout, and password changes
admin.py Account approval
groups.py Registration-group management
lists.py List creation, ownership, and sharing
items.py Item entry, categories, quantities, and completion
api.py Session-based JSON API
models.py SQLAlchemy models
templates/ Jinja templates
static/ CSS, JavaScript, icons, and PWA manifest
instance/ Local secrets and database (generated, ignored by Git)
```
## Development notes
There is currently no automated test suite or migration framework. Before
upgrading an existing installation, back up the database and secrets, then test
the change on a copy of the data.