A cozy, self-hosted, single-user to-do app where Taski — a cute black-and-white pixel-art caretaker — decides what you should work on next, visual-novel style.
Taski greets you, quips at you, and when you ask "give me a task" she picks one using a deliberately semi-random algorithm. Sometimes she hands you something you've neglected for ages, sometimes something you already half-finished, and sometimes just pure luck of the draw. She's sometimes harsh, sometimes silly, sometimes cute — and she has a large bank of dialogue so she stays fresh.
- 🎭 Taski, an emotive pixel character with 9 expressions and hundreds of dialogue lines that react to everything you do.
- ✅ Tasks + subgoals — a task is "done" when you check off all its steps.
- 🎲 "Give me a task" — a semi-random picker with no real priority, just
moods and reasons (
stale,half_done,random). ▶️ One active task at a time — check off steps, pause it (returns to the list with progress kept), or remove it. Every action gets a Taski response.- 🌗 Dark & light themes (grayscale by design; toggle any time).
- 📱 Desktop and mobile friendly.
- 🔒 Single-user login with a password you set.
- 🐳 Dockerized & self-hosted, with a persistent SQLite volume.
docker-compose.yml pulls the prebuilt image from GHCR — no local build needed:
cp .env.example .env # then edit .env and set ATASKTODO_PASSWORD
docker compose up -d # pulls ghcr.io/troglobitten/atasktodo:latestOpen http://localhost:3000 and log in with the password you set.
Prefer to build from source? In
docker-compose.yml, comment out theimage:line and uncommentbuild: ., then rundocker compose up -d --build.
⚠️ If you don't setATASKTODO_PASSWORD, the default istaski— change it before exposing this anywhere.
If port 3000 is already taken (on Windows it can fall in a reserved range), change the left side of the
ports:mapping indocker-compose.yml, e.g."8080:3000", and open that port instead.
Set ATASKTODO_INSECURE_COOKIE=0 in .env when you serve over TLS (e.g. behind
a reverse proxy). Keep it 1 for plain-HTTP LAN use so the login cookie works.
Every push to main builds and publishes a container image to the GitHub
Container Registry via GitHub Actions, so you can skip the build:
docker run -d --name atasktodo \
-p 3000:3000 \
-e ATASKTODO_PASSWORD=change-me-please \
-e ATASKTODO_INSECURE_COOKIE=1 \
-v atasktodo-data:/data \
ghcr.io/troglobitten/atasktodo:latestThe GHCR package is private by default — either
docker login ghcr.iofirst, or flip the package to public under the repo's Packages settings.
npm install
ATASKTODO_PASSWORD=yourpassword npm start
# open http://localhost:3000On Windows, if port 3000 is in a reserved range you can pick another:
$env:ATASKTODO_PASSWORD="yourpassword"; $env:PORT="4000"; npm start| Variable | Default | Purpose |
|---|---|---|
ATASKTODO_PASSWORD |
taski |
Your login password. Change it. |
ATASKTODO_SECRET |
generated | JWT signing secret. If unset, one is generated & stored in DB. |
ATASKTODO_DATA_DIR |
./data |
Where the SQLite file lives (mount a volume in Docker). |
ATASKTODO_INSECURE_COOKIE |
unset | Set 1 for plain HTTP; 0/unset behind HTTPS. |
PORT |
3000 |
HTTP port. |
HOST |
0.0.0.0 |
Bind address. |
All of Taski's art is plain editable text grids — no image editor needed.
public/assets/sprites/sprites.js— every expression is a 16×16 grid of characters you can hand-edit:.= transparent,K= ink (outline/features),W= body fill,C= accent (cheeks).- Colours come from CSS variables, so each sprite auto-inverts for dark/light.
- To add an expression, copy a block, rename it, redraw its 16 rows, and use the new mood name in the dialogue file.
public/assets/sprites/favicon.svg— the browser-tab face, as editable rects.- Colours live in
public/css/styles.css(the--ink,--paper,--accent,--stage,--bgvariables). Change them to reskin everything. - UI icons (check, ✕, +, star, diamond, power, sun) are hand-authored pixel
SVGs in the
PIXEL ICONSblock ofstyles.css, drawn as<rect>pixels and tinted withcurrentColorso they follow the theme. Add one with a--iurl and a.pi-yournameclass, then drop<span class="pi pi-yourname"></span>in.
- The UI uses Doto, a variable
pixel font, self-hosted in
public/assets/fonts/so it works offline. - Weight and roundness are controlled by two variables at the top of
styles.css:--font-wght(default 900) and--font-rond(default 100 = fully round dots). Change those two numbers to restyle all text. - Fallbacks: Bitcount Single (also bundled) → the monospace stack. Swap the
@font-facefiles or the--fontstack to change typefaces entirely.
public/data/dialogue.jsonis the entire script. Each line is{ "mood": "...", "text": "..." }, grouped by situation (greeting, task_added, subgoal_checked, task_completed, task_paused, task_removed, idle, and the nestedtask_givenby reason).moodmust match a sprite name;{task}is replaced with the task title. Add as many lines as you like — the app avoids repeating the last line it used per category.
Each task shows a little pixel face next to its title reflecting how long it has sat untouched:
- 🙂 happy — touched within the last 7 days
- 😐 neutral — 7 to 30 days untouched
- 🙁 sad — 30+ days untouched
The thresholds live in moodStage() in public/js/app.js (just change the two
day numbers). The faces are pixel-art mask icons (.pi-mood-happy/neutral/sad)
in the PIXEL ICONS block of public/css/styles.css, tinted with the theme's
ink colour. Hovering a card shows exactly how many days it's been idle.
server/taski.jsholds the algorithm. AdjustREASON_WEIGHTSto bias her toward stale / half-done / random, orSTALE_MSfor what counts as "a long time." There is intentionally no urgency/priority system.
atasktodo/
├─ server/
│ ├─ index.js Express API + static host
│ ├─ db.js SQLite storage
│ ├─ auth.js single-user login (JWT cookie)
│ └─ taski.js semi-random task-picking algorithm
├─ public/
│ ├─ index.html
│ ├─ css/styles.css
│ ├─ js/ app.js · api.js · taski.js (voice + face controller)
│ ├─ assets/sprites/ sprites.js (EDIT ME) · favicon.svg
│ └─ data/dialogue.json (EDIT ME — Taski's whole script)
├─ Dockerfile
├─ docker-compose.yml
└─ .env.example
Everything lives in one SQLite file inside the atasktodo-data Docker volume
(or ./data/atasktodo.sqlite locally). Back that up and you've backed up
everything.
MIT — do whatever you like. Taski believes in you.