# L@pp 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. 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). ## Features - 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 ## Requirements - Python 3.9 or newer - `pip` and Python virtual-environment support ## Quick start Clone the repository and enter its directory: ```bash git clone 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 . 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: application_key: database_uri: sqlite:///app.db initial_data: admin_password: admin_group: user_password: user_group: fernet: key: ``` 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.