- Use sentinel element to properly clean up detail view Ctrl+E listener - Single document listener per view handles all shortcuts regardless of focus - Ctrl+S/D now work from preview tab and title input without separate listeners - Ctrl+Alt+PageUp/Down switches Write/Preview tabs Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
task-inventory
Personal inventory for tasks, ideas, notes, and anything else. Entries are organised in a hierarchy, with configurable entry types. Designed to grow into project management over time.
Features
- Configurable entry types — default types are task, idea, note; create your own
- Hierarchical entries — unlimited nesting, any type can be a child of any other
- Tree view (roots only, expandable — children always shown unfiltered) and flat view
- Filters by status, priority, and tag; free-text search across title and body; clickable tag chips
- Entry detail view — full rendered markdown, metadata, children list; URL-addressable (
#4) - URL routing —
#all,#type/<id>,#<entry-id>,#manage-types; back/forward works - Markdown rendering via a local Gitea instance — edit/preview tabs in the editor
- Sequential integer IDs — global across all entry types
- Flat NDJSON key-value store — single file, no database required
Setup
npm install
cp config.yaml.example config.yaml
# edit config.yaml — add your Gitea URL and token
node server.mjs
Server starts on http://localhost:3025 by default.
Configuration
config.yaml (not committed):
gitea:
url: https://gitea.example.com
token: your_token_here # needs read:misc scope minimum
If Gitea is not configured, titles and bodies are shown as plain text.
Environment overrides:
| Variable | Default |
|---|---|
PORT |
3025 |
BIND_ADDRESS |
localhost |
Data model
All data is stored in data/tasks.ndjson as a flat NDJSON key-value store.
Entry {
id integer sequential, global across all types
type string entry type id (e.g. "task", "idea", "note")
title string markdown
body string markdown, optional
status open | deferred | done | cancelled
priority high | normal | low
tags string[]
parent_id integer | null null = root entry
created_at ms timestamp
updated_at ms timestamp
}
Entry_Type {
id string lowercase alphanumeric/underscore, e.g. "bug_report"
title string display name
description string
created_at ms timestamp
updated_at ms timestamp
}
On first run, three default entry types are seeded: task, idea, note. Existing data from older flat-key format (task:1, task:2, …) is migrated automatically.
API
All responses: { ok: true, ...data } or { ok: false, error: string }.
GET /api/entry-types
POST /api/entry-types body: { id, title, description? }
PUT /api/entry-types/:id body: { title?, description? }
DELETE /api/entry-types/:id blocked if type still has entries
GET /api/entries ?type=<id> to filter by type
POST /api/entries body: { type, title, body?, status?, priority?, tags?, parent_id? }
GET /api/entries/:id
PUT /api/entries/:id body: any subset of entry fields
DELETE /api/entries/:id blocked if entry has children
POST /api/render-markdown body: { text, mode? } → { html }
Deployment
# First time: create service user and config
sudo useradd -r task-inventory
sudo mkdir -p /srv/task-inventory
sudo cp config.yaml.example /srv/task-inventory/config.yaml
sudo chown task-inventory:task-inventory /srv/task-inventory/config.yaml
# edit /srv/task-inventory/config.yaml
# Deploy (and on every update)
make deploy DEST=/srv/task-inventory
# Enable service (first time only)
sudo systemctl enable --now "$(realpath deployment/task-inventory.service)"
make deploy runs rsync (excluding data/, config.yaml, node_modules/), fixes ownership, and runs npm ci as the service user. make sync does the rsync+chown only, skipping npm ci.
To bind to all interfaces instead of localhost, uncomment Environment=BIND_ADDRESS=0.0.0.0 in deployment/task-inventory.service.
File map
server.mjs Entry point — Express 5, all routes
Makefile deploy / sync targets
lib/
config.mjs Loads config.yaml (ENOENT-safe)
kv-store.mjs Flat NDJSON key-value store (auto-load, debounced flush)
storage.mjs Entry types + entries CRUD, ID generation, migration
public/
app.mjs SPA — state, routing, rendering, dialogs
index.html Shell
style.css All styles
templates.html HTML templates (injected at init)
gitea-markup.css Vendored Gitea markup + chroma CSS for markdown rendering
lib/
api.mjs fetch wrappers for all API endpoints
dom.mjs qs(), clone(), set_text(), show(), hide()
data/ Created at runtime, not committed
tasks.ndjson All entry records and entry type definitions
deployment/
task-inventory.service systemd unit file