Docs: bring SPEC/README/CHANGELOG current through v0.5.0

- SPEC.md: retitled from 'v1 Draft' to a living-document status line;
  added missing Schema Reference sections for gtd-kanban (2.3) and
  gtd-matrix (2.4, both modes) — previously undocumented outside the
  roadmap; added unscheduled-todos/unscheduled-notes to the calendar
  table; rewrote Rendering (§4) and Architecture (§5) to reflect the
  three-block pipeline, shared collectors, and tag-fetch machinery;
  corrected v0.2.0 roadmap heading (recurrence shipped, strip horizon
  was not built — previously overclaimed)
- New CHANGELOG.md: one entry per shipped version, 0.1.0 through 0.5.0
- README.md: fixed stale test count (47 -> 88), linked CHANGELOG, added
  a 'Cutting a release' checklist including the npm whoami / E404
  auth-expiry gotcha hit across several publishes
- No source changes; 88 tests still passing
This commit is contained in:
Victor Wiebe 2026-07-03 14:19:20 +00:00 committed by Victor Wiebe
parent 6bb3227574
commit 9780d95223
4 changed files with 159 additions and 38 deletions

51
CHANGELOG.md Normal file
View File

@ -0,0 +1,51 @@
# Changelog
All notable changes to GTD Calendar are documented here. Versions follow the plugin's `manifest.json` / `package.json` version, which also matches the published npm releases.
## 0.5.0 — The Skeleton Matrix
- Added `mode: skeleton | eisenhower` to `gtd-matrix` (default: `skeleton`).
- **Skeleton mode** replaces Eisenhower's opinion-based axes with derived ones: rows = active (`in-progress-tag`, shared with the kanban), columns = due soon (`urgent-window` days, default 3, overdue included; `urgent-tag` as a manual override for dateless items). Quadrants: **Do Next**, **Scheduled**, **On Deck**, **Backlog** — nothing is ever "Eliminate".
- Classic Eisenhower preserved via `mode: eisenhower`.
- `MatrixBoard` is now positional internally (top-left…bottom-right); labels are supplied per mode, and rendering is fully shared between the two modes.
## 0.4.0 — Eisenhower Matrix
- New `gtd-matrix` block: a 2×2 prioritisation grid with labelled axes.
- Quadrants from two tags (`urgent-tag`, `important-tag`): Do First / Schedule / Delegate / Eliminate.
- Completed to-dos excluded entirely (a prioritisation view, not a tracking view).
- Cards reuse the kanban's rendering (compact, recurrence mark, drilldown, `card-detail`).
## 0.3.2 — Unscheduled visibility toggles
- Added `unscheduled-todos` and `unscheduled-notes` options to `gtd-calendar` (default: `yes` for both; also accepts `true`/`false`, `show`/`hide`).
- Lets a dashboard with multiple stacked calendar blocks show each Unscheduled sub-section under only one of them, instead of duplicating the list.
## 0.3.1 — Split Unscheduled section
- The Unscheduled area now shows two labelled sub-sections — **Unscheduled to-dos** (first) and **Unscheduled notes** — instead of one mixed list.
- A sub-section is omitted entirely when it has no items.
## 0.3.0 — Kanban board
- New `gtd-kanban` block: three columns (Backlog / In Progress / Done) built from to-dos only.
- Bucketing: `todo_completed` wins → Done; else the `in-progress-tag` → In Progress; else → Backlog.
- Done column limited to a configurable `done-window` (default 7 days; `all` for full history).
- Per-column sort (`due-date` / `title` / `modified-date`); dateless cards sort last under `due-date`.
- Compact cards with a configurable detail panel (`card-detail: hover | always | none`).
- Read-only.
## 0.2.0 — Recurring to-do awareness
- Recurring to-dos (as managed by the *Repeating To-Dos* plugin, v2) are marked with a ↻ symbol across every view.
- Detected via the `recurring` tag the Repeating To-Dos plugin maintains as its own query index — no coupling to its note-body format, no code duplication of its recurrence logic.
## 0.1.0 — Initial release
- Forked from [WeMakeMachines/joplin-plugin-event-calendar](https://github.com/WeMakeMachines/joplin-plugin-event-calendar).
- `gtd-calendar` block populated by real notes and to-dos (instead of hand-written YAML) in the calendar note's folder and, optionally, its descendants (`scope`).
- Views: `day`, `week`, `month` (compact tile strip, upstream-style) and `month-grid` (full current-month 7-column grid with weekday headers).
- Notes and to-dos opt in via a `gtd` fenced block (empty block valid); to-dos schedule automatically from their Joplin due date.
- Unscheduled items (no resolvable date) collect below the grid.
- Drilldown: every tile opens its source note.
- Fixed an upstream bug where the week view silently grouped by month.

View File

@ -189,11 +189,19 @@ Issues and ideas: [the repository](https://gitea.skeletonworks.online/vwiebe/jop
```bash ```bash
npm install npm install
npm test # 47 unit tests npm test # 88 unit tests
npm run dist # builds publish/*.jpl npm run dist # builds publish/*.jpl
``` ```
Design notes and the full v1 specification live in [SPEC.md](SPEC.md). Design notes and the full specification live in [SPEC.md](SPEC.md). Release history is in [CHANGELOG.md](CHANGELOG.md).
### Cutting a release
1. Bump `version` in **both** `src/manifest.json` and `package.json` (they must match — the Joplin plugin repo reads the manifest, npm reads package.json).
2. Add an entry to `CHANGELOG.md`.
3. `npm publish` (the `prepare` script rebuilds the `.jpl` fresh from source, so you can't accidentally publish a stale build).
If `npm publish` fails with `E404` on the `PUT`, check `npm whoami` first — a 401 there means your login session has expired, which npm surfaces as a misleading 404 on publish. Fix with `npm login --auth-type=legacy`, or avoid it entirely with a [granular access token](https://docs.npmjs.com/creating-and-viewing-access-tokens) so publishing never needs an interactive session.
## Credits & license ## Credits & license

130
SPEC.md
View File

@ -1,9 +1,11 @@
# GTD Calendar — Project Specification (v1 Draft) # GTD Calendar — Project Specification
A fork of [WeMakeMachines/joplin-plugin-event-calendar](https://github.com/WeMakeMachines/joplin-plugin-event-calendar) (MIT) that inverts the plugin's data model: instead of events living as YAML inside the calendar note, the calendar is **populated by real Joplin notes and todos** in the surrounding folder tree, with clickable drilldown to the source note. *Status: living document, current through v0.5.0. Sections 15 describe the shipped design; §9 tracks what's still ahead.*
**Working name:** `joplin-plugin-gtd-calendar` A fork of [WeMakeMachines/joplin-plugin-event-calendar](https://github.com/WeMakeMachines/joplin-plugin-event-calendar) (MIT) that inverts the plugin's data model: instead of events living as YAML inside the calendar note, the plugin's views are **populated by real Joplin notes and todos** in the surrounding folder tree, with clickable drilldown to the source note. What started as a single calendar view has grown into three composable views — calendar, kanban, and a priorities matrix — sharing one data layer and one tag/date vocabulary.
**Proposed manifest id:** `com.victorwiebe.joplin.plugin.gtd-calendar` (must differ from upstream's id so both plugins can coexist during development)
**Package name:** `joplin-plugin-gtd-calendar` (npm)
**Manifest id:** `com.victorwiebe.joplin.plugin.gtd-calendar`
--- ---
@ -39,8 +41,12 @@ sort-type: title # title | modified_date (default: title)
| `todos` | `gtd-only`, `all`, `none` | `gtd-only` | Same semantics for todos. | | `todos` | `gtd-only`, `all`, `none` | `gtd-only` | Same semantics for todos. |
| `sort` | `asc`, `desc` | `asc` | Order of events **within** each day/week/month grouping and within Unscheduled. Groupings themselves always run chronologically. | | `sort` | `asc`, `desc` | `asc` | Order of events **within** each day/week/month grouping and within Unscheduled. Groupings themselves always run chronologically. |
| `sort-type` | `title`, `modified_date` | `title` | Field used by `sort`. `modified_date` maps to Joplin's `updated_time`. | | `sort-type` | `title`, `modified_date` | `title` | Field used by `sort`. `modified_date` maps to Joplin's `updated_time`. |
| `unscheduled-todos` | `yes`/`no` (also `true`/`false`, `show`/`hide`) | `yes` | Whether the Unscheduled **to-dos** sub-section is shown. |
| `unscheduled-notes` | `yes`/`no` (also `true`/`false`, `show`/`hide`) | `yes` | Whether the Unscheduled **notes** sub-section is shown. Independent of `unscheduled-todos` — lets a dashboard split the two lists across stacked calendar blocks instead of duplicating both. |
Unknown keys are ignored with a console warning. A malformed block renders an inline error box rather than failing silently (improvement over upstream, which swallows parse errors into the console). Unscheduled items (no resolvable date) are split into two labelled sub-sections — to-dos first, then notes — each hidden when empty or toggled off.
Unknown keys are ignored with a warning. A malformed block renders an inline warning above the calendar rather than failing silently.
### 2.2 The `gtd` block (lives in participating notes/todos) ### 2.2 The `gtd` block (lives in participating notes/todos)
@ -66,6 +72,47 @@ text: Hover text # optional; tooltip detail, as upstream
An **empty** ` ```gtd ` block is valid and meaningful: it is the opt-in signal with all defaults. An **empty** ` ```gtd ` block is valid and meaningful: it is the opt-in signal with all defaults.
### 2.3 The `gtd-kanban` block (lives in the board note)
```
```gtd-kanban
title: Editorial Board
scope: this-folder # same semantics as gtd-calendar
todos: gtd-only # gtd-only | all | none (default: gtd-only)
sort-type: due-date # due-date | title | modified-date (default: due-date)
sort: asc # asc | desc (default: asc)
in-progress-tag: in-progress # default: "in-progress"
card-detail: hover # hover | always | none (default: hover)
done-window: 7 # days, or "all" (default: 7)
```
Three columns, always shown: **Backlog**, **In Progress**, **Done**. To-dos only — plain notes never appear. Bucketing: `todo_completed > 0` → Done (wins regardless of tags); else the `in-progress-tag` → In Progress; else → Backlog. Backlog shows everything uncompleted and untagged, dated or not. Done is filtered to to-dos completed within `done-window` days (`all` for full history), preventing unbounded growth. Sorting is per-column; under `due-date`, cards without a due date sort after all dated ones regardless of direction. Read-only in the current version — reflects to-do state, does not change it.
### 2.4 The `gtd-matrix` block (lives in the matrix note)
```
```gtd-matrix
mode: skeleton # skeleton | eisenhower (default: skeleton)
title: Priorities
scope: this-folder
todos: gtd-only
sort-type: due-date
sort: asc
urgent-tag: urgent # default: "urgent"
important-tag: important # Eisenhower mode only; default: "important"
in-progress-tag: in-progress # Skeleton mode only; default: "in-progress"
urgent-window: 3 # Skeleton mode only, days; default: 3
card-detail: hover
```
A 2×2 grid with labelled axes. To-dos only; **completed to-dos are excluded entirely** in both modes (a prioritisation view, not a tracking view — the kanban's Done column is where completions live). The board is internally positional (top-left / top-right / bottom-left / bottom-right); each mode supplies its own axis and quadrant labels.
**`mode: skeleton`** (default) — rows = active (`in-progress-tag`, the same tag the kanban uses); columns = due soon (due date within `urgent-window` days, overdue included, or the `urgent-tag` as a manual override for dateless items). Quadrants: **Do Next** (active + due soon), **Scheduled** (active, not due soon — includes dateless in-progress work), **On Deck** (not active + due soon — the early-warning quadrant), **Backlog** (neither). Designed to complement the calendar and kanban directly: same in-progress tag, dates drive urgency instead of a second opinion-tag, and nothing is ever condemned as "Eliminate" — a Backlog item is simply low priority. In-progress + `urgent-tag` + no date resolves to Do Next (the urgent tag is an explicit override, so it wins over the absence of a date).
**`mode: eisenhower`** — the classic: rows = `important-tag`; columns = `urgent-tag`. Quadrants: Do First (both), Schedule (important only), Delegate (urgent only), Eliminate (neither).
A warning is raised if `urgent-tag` and `important-tag` resolve to the same tag (Eisenhower mode only — the axes would collapse). Read-only.
--- ---
## 3. Data Model Rules ## 3. Data Model Rules
@ -96,59 +143,74 @@ The calendar note itself is always excluded from results, as is any note whose o
## 4. Rendering ## 4. Rendering
- Reuses upstream's grouping/renderer architecture (`DayGrouping`/`WeekGrouping`/`MonthGrouping` + DOM renderers + CSS asset), including the empty-grouping placeholders and current-date highlight. - **Calendar** reuses upstream's grouping/renderer architecture (`DayGrouping`/`WeekGrouping`/`MonthGrouping` + DOM renderers + CSS asset), including the empty-grouping placeholders and current-date highlight, plus the fork's own `month-grid` (a 7-column grid of the entire current month).
- **Fixes upstream bug:** `Calendar/index.ts` imports `WeekGrouping` from `Month/MonthGrouping`, so the week view silently groups by month. Corrected in the fork. - **Fixed upstream bug:** `Calendar/index.ts` imported `WeekGrouping` from `Month/MonthGrouping`, so the week view silently grouped by month. Corrected in the fork.
- **Type distinction:** todos render with a checkbox glyph (☐ / ☑ when completed); notes render with a document glyph. Glyphs are suppressed if the item supplies its own `icon`. A subtle border-style difference (solid vs. dashed, TBD in CSS) keeps the distinction visible under custom colours. - **Type distinction:** todos render with a checkbox glyph (☐ / ☑ when completed); notes render with a document glyph. Glyphs are suppressed if the item supplies its own `icon`. Recurring to-dos (per the `recurring` tag) get a ↻ suffix to the right of any icon, across every view.
- **Unscheduled section:** pinned below the grid, with its own header, honoring `sort` / `sort-type`. - **Unscheduled section:** below the calendar grid, split into two labelled sub-sections — to-dos first, then notes — each hidden when empty or switched off (`unscheduled-todos` / `unscheduled-notes`).
- **Drilldown:** each tile is clickable and opens the source note. - **Kanban** renders three fixed columns (Backlog / In Progress / Done) of compact cards; a "hover" mode shows due date and hover text only on mouseover (`card-detail`).
- **Matrix** renders a 2×2 grid with axis and quadrant labels supplied per-mode by the plugin process; cards are visually and behaviourally identical to kanban cards.
- **Drilldown:** every tile, card, hover-card row, and chip across all three block types is clickable and opens the source note.
--- ---
## 5. Architecture ## 5. Architecture
Upstream is a single synchronous markdown-it content script with no data API access. The fork becomes a three-part plugin: Upstream is a single synchronous markdown-it content script with no data API access. The fork is a three-part plugin, now serving three fence types (`gtd-calendar`, `gtd-kanban`, `gtd-matrix`) through the same pipeline:
``` ```
┌─────────────────────────────┐ ┌─────────────────────────────┐
│ 1. Markdown-it content │ Intercepts ```gtd-calendar fences. │ 1. Markdown-it content │ Intercepts ```gtd-calendar / ```gtd-kanban /
│ script (renderer) │ Parses YAML config, emits a placeholder │ script (renderer) │ ```gtd-matrix fences. Parses YAML config,
│ │ <div> carrying the config as data-attrs. │ │ emits a placeholder <div> tagged with
│ │ data-block-type and the config as a
│ │ data attribute.
└──────────────┬──────────────┘ └──────────────┬──────────────┘
│ (rendered HTML) │ (rendered HTML)
┌──────────────▼──────────────┐ ┌──────────────▼──────────────┐
│ 2. Webview asset script │ Finds placeholders, calls │ 2. Webview asset script │ Finds placeholders, calls
│ (runs in rendered note) │ webviewApi.postMessage(config). │ (runs in rendered note) │ webviewApi.postMessage(config) with
│ │ Receives event data, builds the │ │ getEvents / getKanban / getMatrix
│ │ calendar DOM via the (reused) │ │ depending on block type. Builds the
│ │ grouping/renderer classes. │ │ calendar / board / matrix DOM from the
│ │ Click → postMessage({open: noteId}). │ │ returned payload. Click → postMessage
│ │ ({type: "openNote", noteId}).
└──────────────┬──────────────┘ └──────────────┬──────────────┘
│ postMessage / onMessage │ postMessage / onMessage
┌──────────────▼──────────────┐ ┌──────────────▼──────────────┐
│ 3. Main plugin process │ joplin.contentScripts.onMessage: │ 3. Main plugin process │ joplin.contentScripts.onMessage routes
│ (index.ts) │ • resolve current note's folder │ (index.ts) │ by message type to a handler per block
│ │ type, each of which:
│ │ • resolves current note's folder
│ │ (joplin.workspace.selectedNote) │ │ (joplin.workspace.selectedNote)
│ │ • build folder tree, apply scope │ │ • builds folder tree, applies scope
│ │ • fetch notes/todos (id, title, │ │ (src/Gtd/folderScope.ts)
│ │ is_todo, todo_due, todo_completed, │ │ • fetches notes/todos, paginated
│ │ updated_time, body) │ │ • parses ```gtd blocks from bodies
│ │ • parse ```gtd blocks from bodies │ │ (src/Gtd/gtdBlock.ts)
│ │ • apply inclusion matrix, return │ │ • fetches tag titles per to-do
│ │ structured event list │ │ (getNoteTagTitles) for recurrence /
│ │ • handle open: joplin.commands │ │ in-progress / urgent / important
│ │ .execute('openNote', noteId) │ │ detection
│ │ • applies the inclusion matrix, date
│ │ resolution, and view-specific
│ │ bucketing (collectEvents.ts /
│ │ collectKanban.ts / collectMatrix.ts)
│ │ • handles openNote via
│ │ joplin.commands.execute
└─────────────────────────────┘ └─────────────────────────────┘
``` ```
All three collectors (`collectEvents`, `collectKanban`, `collectMatrix`) share `folderScope.ts`, `gtdBlock.ts`, and `resolveDate.ts` — the inclusion-matrix and date-resolution logic is written once and reused by every view.
### Performance notes ### Performance notes
- Folder tree and note list fetched per render request; paginated API calls (`page`/`has_more`). - Folder tree and note list fetched per render request; paginated API calls (`page`/`has_more`).
- Body scanning is limited to items in scope. For large scopes, pre-filter with Joplin's search API (`body:"```gtd"`-style query) before fetching full bodies. Cache by `updated_time` if needed; not a v1 requirement. - Body scanning is limited to items in scope. Kanban and matrix additionally fetch tag titles per to-do (one call each) for recurrence/state detection. For large scopes, pre-filter with Joplin's search API before fetching full bodies; cache by `updated_time` if needed — not yet required at observed real-world scales (largest tested: ~285 notes, single folder scope), but worth revisiting if a `children`-scope board on a large tree feels slow, especially with Persistent Layout keeping several boards rendered at once.
- Refresh model: re-render occurs on note switch (Joplin behavior). `joplin.workspace.onNoteChange` live refresh is a stretch goal, not v1. - Refresh model: re-render occurs on note switch (Joplin behavior); no live refresh on background data changes. Fine for a read-only plugin; becomes a sharper question once the authoring axis (§9) lands, where a write on one rendered board should ideally refresh others.
### Platform caveats ### Platform caveats
- Desktop is the target. Mobile plugin support is partial; `openNote` behavior there is unverified. The "embed search" pairing remains a documented fallback for mobile users. - Desktop is the target. Mobile plugin support is partial and this plugin's core mechanisms (custom fence rendering, webview↔plugin messaging) are not expected to work there; documented explicitly in the README. Underlying to-dos still sync and appear in Joplin's native mobile views. The "Embed Search" plugin pairing remains a documented pattern for cross-scope aggregation generally, not specifically a mobile workaround.
--- ---
@ -184,7 +246,7 @@ Upstream is MIT (© Franco Speziali / WeMakeMachines). The fork retains the MIT
Recorded direction, not commitments. Each gets a focused design pass before work begins; real-world use of the shipped plugin may reorder this. Recorded direction, not commitments. Each gets a focused design pass before work begins; real-world use of the shipped plugin may reorder this.
### v0.2.0 — Repeating to-dos (visualisation) + configurable strip horizon ### v0.2.0 — Repeating to-dos (visualisation) — SHIPPED · strip horizon — NOT YET BUILT
Recurrence itself is delegated to an existing community plugin (open-environment approach; we don't reimplement it). Candidates evaluated: BeatLink's original *Repeating To-Dos* (unmaintained), the *[Revived]* fork (maintained, GitHub issues only), and *Repeating To-dos v2* by TheScriptingGuy (`com.github.TheScriptingGuy.joplin-plugin-repeating-todos`, best-documented). All share the same mechanic: on completion the to-do's alarm date is reset to the next occurrence and the to-do is unmarked — so at any moment a recurring to-do is a **single** to-do at its next due date, with the recurrence rule stored in the note body. Recurrence itself is delegated to an existing community plugin (open-environment approach; we don't reimplement it). Candidates evaluated: BeatLink's original *Repeating To-Dos* (unmaintained), the *[Revived]* fork (maintained, GitHub issues only), and *Repeating To-dos v2* by TheScriptingGuy (`com.github.TheScriptingGuy.joplin-plugin-repeating-todos`, best-documented). All share the same mechanic: on completion the to-do's alarm date is reset to the next occurrence and the to-do is unmarked — so at any moment a recurring to-do is a **single** to-do at its next due date, with the recurrence rule stored in the note body.

4
package-lock.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "joplin-plugin-gtd-calendar", "name": "joplin-plugin-gtd-calendar",
"version": "0.4.0", "version": "0.5.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "joplin-plugin-gtd-calendar", "name": "joplin-plugin-gtd-calendar",
"version": "0.4.0", "version": "0.5.0",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"date-fns": "^2.29.3", "date-fns": "^2.29.3",