mikael-lovqvists-claude-agent 7d8dcf85ce Fix tree filter and URL canonicality for filter state
- Root entries in tree view filtered strictly by entry_matches_filter;
  a parent no longer appears just because a descendant matches
- Expanded children always shown unfiltered (tree navigation intent)
- build_url always writes ?status=<val> so URL is fully canonical;
  apply_search falls back to open for absent/empty param (old URLs)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 16:21:46 +00:00

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
Description
Personal task management
Readme 368 KiB
Languages
JavaScript 76%
CSS 17.6%
HTML 5.4%
Makefile 1%