146 lines
5.6 KiB
Markdown
146 lines
5.6 KiB
Markdown
# 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 shortcuts** — `Ctrl+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
|
|
|
|
```bash
|
|
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):
|
|
|
|
```yaml
|
|
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
|
|
|
|
```bash
|
|
# 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
|
|
```
|