- Python 66.6%
- HTML 22.7%
- Go Template 6.8%
- JavaScript 2%
- Dockerfile 1.9%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
|
||
| static | ||
| tech-kanban | ||
| templates | ||
| tests | ||
| .dockerignore | ||
| .env.example | ||
| .gitignore | ||
| .python-version | ||
| .woodpecker.yml | ||
| compose.yaml | ||
| database.py | ||
| Dockerfile | ||
| main.py | ||
| pyproject.toml | ||
| README.md | ||
| uv.lock | ||
Tech Kanban
A simple shared Flask Kanban board backed by MariaDB. Users can create, edit, assign, move, and delete tickets across To do, In progress, and Done. Administrators create accounts, change roles, disable/enable access, delete accounts, and reset passwords. Everyone can change their own username and password through Account by confirming their current password. There is no public registration. Passwords are hashed, forms use CSRF protection, and disabling an account or resetting its password revokes sessions.
Run locally
Requires Python 3.12+, uv, and Docker Compose (or an existing MariaDB server).
- Copy
.env.exampleto.env. Replace the passwords and secret key with random values; keep the password inDATABASE_URLconsistent withDB_PASSWORD. Generate a secret withpython3 -c 'import secrets; print(secrets.token_hex(32))'. - Start MariaDB and install dependencies:
docker compose up -d --wait uv sync --locked - Export your environment, create the tables, and bootstrap the first admin:
The command prompts for a username and password (at least 12 characters). It only works when there are no accounts. All subsequent accounts must be created by an administrator through Users.set -a source .env set +a uv run flask --app main:create_app init-db uv run flask --app main:create_app create-admin - Start the app:
Visit http://127.0.0.1:5000 and sign in.uv run flask --app main:create_app run
All authenticated users share one board and can manage all tickets. Disable users instead of deleting them to preserve ticket assignments. Deleting an account requires confirmation, revokes access, and leaves its tickets unassigned. Admins cannot disable, demote, or delete themselves. Password changes through Account keep the current session and sign out other sessions. Admin password resets sign out all sessions, including your own if you reset your own password.
MariaDB data persists in the mariadb_data Docker volume. init-db creates missing
tables; it does not migrate existing schemas. Back up the database before future
schema changes. For an existing server, supply its connection in DATABASE_URL;
URL-encode any special characters in credentials.
Checks
If tickets created by the older API count toward the total but do not appear in
any column, repair their legacy open status to todo after deploying the fix:
kubectl -n tech-kanban exec deployment/tech-kanban -- \
/app/.venv/bin/python -m flask --app main:create_app repair-ticket-statuses
This command only changes open tickets and is safe to run again.
uv run pytest
uvx ruff check .
uvx ruff format --check .
uvx ty check
Tests use isolated SQLite databases to exercise authentication, authorization,
ticket persistence, session revocation, and CSRF without needing a server.
Set TEST_DATABASE_URL to an empty, disposable MariaDB database to run the same
suite against MariaDB. Tests create and drop tables in that database.
For deployment, run behind HTTPS using a production WSGI server, set
COOKIE_SECURE=true, retain a stable secret, and apply login rate limiting at the
reverse proxy. The Flask development server is for local use.
SQLite development mode
No MariaDB or environment configuration is needed:
uv sync --locked
uv run main.py dev create-admin
uv run main.py dev
The first argument dev explicitly selects SQLite and enables Flask's local debug
server. Both commands create missing tables automatically. Data lives in
instance/kanban.db; a persistent development signing key lives in
instance/dev-secret. Both are ignored by Git and excluded from container builds.
The same models and account permissions apply to both databases; there are no
seeded users or default passwords. Development mode ignores MariaDB environment
settings. Normal startup still requires the production configuration.
Kubernetes / Helm
The chart in tech-kanban/ follows the registry, Traefik, secret, and persistent
MariaDB conventions in the CTF example.
It includes a standalone MariaDB StatefulSet (no chart dependencies), a persistent
volume, database initialization, health probes, and a Gunicorn application image.
Build and push your application image to your chosen registry:
docker build -t YOUR_REGISTRY/tech-kanban:0.1.0 .
docker push YOUR_REGISTRY/tech-kanban:0.1.0
kubectl create namespace tech-kanban
Create a Secret in that namespace with keys mariadb-password and
mariadb-root-password. For example, using temporary files avoids putting
passwords into shell history or command arguments:
secret_dir=$(mktemp -d)
python3 -c 'import secrets; print(secrets.token_hex(32), end="")' > "$secret_dir/mariadb-password"
python3 -c 'import secrets; print(secrets.token_hex(32), end="")' > "$secret_dir/mariadb-root-password"
kubectl -n tech-kanban create secret generic mariadb-secret --from-file="$secret_dir"
rm -r "$secret_dir"
Copy tech-kanban/values-platform4.example.yaml to a local values file and adapt
its image repository, hostname, TLS secret and storage class. Create the referenced
gitlab-registry image-pull secret and TLS secret in the same namespace (or remove
the image-pull-secret reference for a public registry). Then install:
helm upgrade --install tech-kanban ./tech-kanban \
--namespace tech-kanban -f YOUR_VALUES.yaml --wait --timeout 10m
kubectl -n tech-kanban exec -it deployment/tech-kanban -- \
/app/.venv/bin/python -m flask --app main:create_app create-admin
The init container creates tables before Gunicorn starts. MariaDB advisory locking
serializes initialization across replicas; it is not a schema migration system.
The first admin is deliberately created interactively, never from default credentials.
The chart generates a session signing key and reuses the existing Secret during
Helm upgrades. For GitOps rendering, supply a stable externally managed Secret via
secretKey.existingSecret and set secretKey.create=false instead.
The chart also generates a separate API token Secret and injects it as API_TOKEN
into the application and initialization containers. Helm upgrades reuse the existing
token. The ticket API expects the raw token in the Authorization header (without
a Bearer prefix). To supply your own token or use GitOps rendering, create a
Kubernetes Secret containing an api-token key and configure:
apiToken:
create: false
existingSecret: kanban-api-token
key: api-token
Restart the application pods after changing an externally managed token.
To use an existing MariaDB server, set mariadb.enabled=false and configure
database.host, port, name, user, secretName, and passwordKey.
The app accepts those as DB_HOST, DB_PORT, DB_NAME, DB_USER, and DB_PASSWORD;
DATABASE_URL takes precedence outside the default chart configuration. Separate
credentials are escaped by SQLAlchemy, so special characters in passwords work.
HTTPS cookies are enabled by default. For local port-forward testing, override
cookieSecure=false. /health/live checks the app, while /health/ready checks
database connectivity. Application replicas share MariaDB; SQLite is only for
local development. Database PVCs survive uninstall; back them up and manage their
cleanup separately. Secret updates require an app restart; database password
rotation also requires updating the actual database user.
Validate chart changes with helm lint ./tech-kanban and
helm template tech-kanban ./tech-kanban. No deployment is performed by the tests.
Updating the deployed image
CI publishes both latest and sha-<first-seven-commit-characters>. Deploy the
unique SHA tag so Kubernetes runs the intended build even with IfNotPresent:
helm upgrade --install tech-kanban ./tech-kanban -n tech-kanban \
-f YOUR_VALUES.yaml --set-string image.tag=sha-YOUR_SHA \
--wait --timeout 10m
Wait for the image build to finish before upgrading. The Docker build verifies
that Flask and Gunicorn run as the container user. The init container invokes
/app/.venv/bin/python -m flask, avoiding dependence on the runtime PATH.