Simple light Trello-like
  • JavaScript 42.1%
  • Python 34.3%
  • CSS 18.8%
  • HTML 4.8%
Find a file
2026-06-10 16:19:49 -06:00
static Initial commit 2026-06-10 16:19:49 -06:00
templates Initial commit 2026-06-10 16:19:49 -06:00
.gitignore Initial commit 2026-06-10 16:19:49 -06:00
app.py Initial commit 2026-06-10 16:19:49 -06:00
instructions.md Initial commit 2026-06-10 16:19:49 -06:00
README.md Initial commit 2026-06-10 16:19:49 -06:00
requirements.txt Initial commit 2026-06-10 16:19:49 -06:00
twilight.service Initial commit 2026-06-10 16:19:49 -06:00

🌒 Twilight Task System

A tiny single-writer Trello-ish task board. One person writes; everyone else gets read-only share links. Live updates over websocket, dark UI, drag & drop.

Run

python3 -m venv .venv
.venv/bin/pip install -r requirements.txt
TWILIGHT_PASSWORD=yourpassword .venv/bin/python app.py
# -> http://localhost:5000

Binds to 127.0.0.1:5000 by default (intended to sit behind a reverse proxy like Caddy). Override with HOST / PORT env vars, e.g. HOST=0.0.0.0 to expose it directly on your LAN.

Password

Resolved in this order:

  1. $TWILIGHT_PASSWORD
  2. File at $TWILIGHT_PASSWORD_FILE
  3. instance/password.txt — auto-generated on first run (printed to stdout)

Run as a service (systemd)

A unit file is included at twilight.service. Adjust the paths at the top (WorkingDirectory, ExecStart, ReadWritePaths) to wherever you deploy, then:

sudo useradd -r -s /usr/sbin/nologin twilight   # once
sudo chown -R twilight:twilight /opt/twilight/instance
sudo cp twilight.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now twilight

Set the password via the commented Environment= / EnvironmentFile= lines in the unit, or let the app generate one into instance/password.txt (journalctl -u twilight will show it on first start).

How it works

  • Backend: Flask + flask-sock (websocket) + sqlite (instance/twilight.db, created automatically). All mutations are plain HTTP POSTs; the websocket is only used to broadcast "something changed, refetch" to viewers of the same project (best effort).
  • Frontend: plain JS (no build step). State is held client-side; UI updates are applied optimistically and synced lazily to the backend. SortableJS powers drag & drop, marked + DOMPurify render task markdown. Vendored libs live in static/vendor/.

Auth model

  • Writer: logs in via the password page; gets a session cookie. Full CRUD, project switcher, share-link management.
  • Reader: visits /?token=... with an active access token. Read-only view of that one project — no switching, no editing. Tokens are managed from the "Share" button in the banner and can be disabled or deleted at any time.

Layout

app.py               # the whole backend
templates/           # index.html (app shell) + login.html
static/app.js        # all frontend logic
static/style.css     # dark theme
static/vendor/       # Sortable.min.js, marked.min.js, purify.min.js
instance/            # sqlite db, secret key, generated password (gitignored)
twilight.service     # systemd unit (edit paths, copy to /etc/systemd/system)

API sketch

Endpoint Verb Who Notes
/api/state/<pid> GET writer or token full project snapshot
/api/projects[/<id>][/delete] POST writer create / update / delete
/api/lists[/<id>][/delete] POST writer create / update / delete
/api/tasks[/<id>][/delete] POST writer create / update / delete
/api/reorder POST writer bulk task list/order after drag
/api/tokens[/<id>][/delete] POST writer share-link management
/ws?project_id=N[&token=…] WS writer or token change notifications