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 (root entries filtered; expanded children always shown) and flat view
  • Filters by status, priority, and tag; free-text search across title and body; filter state stored in URL query params (?status=open&priority=high&search=foo); status defaults to open
  • Clickable chips — type, status, priority, and tag badges are all clickable to filter or navigate
  • CodeMirror 6 editor — markdown syntax highlighting, multi-cursor, write/preview tabs
  • Keyboard shortcutsCtrl+E edit, Ctrl+S save, Ctrl+D discard, Ctrl+Alt+PgUp/Down switch write/preview tab
  • Entry detail view — full rendered markdown, metadata, children list; URL-addressable (#4)
  • URL routing# (all), #type/<id>, #type/<id>/tag/<tag>, #tag/<tag>, #<entry-id>, #edit/<entry-id>, #manage-types; back/forward works
  • Markdown rendering via a local Gitea instance
  • Sequential integer IDs — global across all entry types
  • Flat NDJSON key-value store — single file, no database required

Setup

npm install
make build                   # bundle CodeMirror vendor lib
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, markdown is 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 (including type)
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) — builds vendor bundle, rsyncs, runs npm ci
make deploy DEST=/srv/task-inventory

# Enable service (first time only)
sudo systemctl enable --now "$(realpath deployment/task-inventory.service)"

make deploy builds the CodeMirror vendor bundle, rsyncs files (excluding data/, config.yaml, node_modules/), fixes ownership, and runs npm ci as the service user. make sync does the rsync+chown only, skipping build and 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                    build / deploy / sync targets
codemirror-entry.mjs        ESM entry point for esbuild CodeMirror bundle
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
  vendor/                   Built by make build, not committed
    codemirror-bundle.mjs   Bundled CodeMirror 6 (ESM)
  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
Description
Personal task management
Readme 368 KiB
Languages
JavaScript 76%
CSS 17.6%
HTML 5.4%
Makefile 1%