diff --git a/api/tree.ts b/api/tree.ts index 10918d0..948e219 100644 --- a/api/tree.ts +++ b/api/tree.ts @@ -1,4 +1,4 @@ -import { readFileSync, readdirSync, writeFileSync, statSync } from 'node:fs'; +import { existsSync, readFileSync, readdirSync, writeFileSync, statSync } from 'node:fs'; import { join, resolve } from 'node:path'; // The todo tree: `todo//.md`, one item per file, frontmatter + hand-written prose. @@ -88,6 +88,16 @@ export function patchFrontmatter(text: string, updates: Record): export function readTree(root: string): { themes: TodoTheme[] } { const themes = readdirSync(root) .filter((d) => statSync(join(root, d)).isDirectory()) + /* + A THEME is a directory with an `_index.md`. Anything else under the folder is ignored. + + This used to assume every subdirectory was one, which was safe when the only possible root was the + platform's own `todo/`. It is not safe now: point a project at a repository root — an easy mistake, + since `todo/` sits inside one — and this read `src/_index.md`, threw ENOENT, and the board showed a + 500 that the panel reported as a permission problem. Skipping is also just correct for a real todo + folder, which may hold a README or a script beside its themes. + */ + .filter((d) => existsSync(join(root, d, '_index.md'))) .sort() .map((theme) => { const files = readdirSync(join(root, theme)).filter((f) => f.endsWith('.md')); diff --git a/manifest.ts b/manifest.ts index d9efc7d..a98d28a 100644 --- a/manifest.ts +++ b/manifest.ts @@ -33,7 +33,7 @@ import type { PluginManifest } from '@@/plugins/manifest'; */ export const manifest: PluginManifest = { publisher: 'officerdev', - version: '2026.08.29.01', + version: '2026.08.29.02', platform: '>=1.0.0', label: 'Kanban', diff --git a/web/BoardPanel.tsx b/web/BoardPanel.tsx index 0015378..ba16b3e 100644 --- a/web/BoardPanel.tsx +++ b/web/BoardPanel.tsx @@ -5,6 +5,7 @@ import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; import { AlertTriangle, CalendarClock, X } from 'lucide-react'; import { cn } from '@/lib/utils'; +import { errorText } from 'helpers/error-text'; import type { TodoItem } from './useKanban'; import { useSelectedProject, useTodoItem, useTodoPatch, useTodoTree } from './useKanban'; @@ -54,16 +55,52 @@ export const BoardPanel = () => { return qs ? `?${qs}` : '.'; }; - if (error) { + if (project === null) { return (
- The todo board is owner-only, and this account was refused. Nothing here is broken — there is simply nothing to - show. + Choose a project on the left, or add one. +
+ ); + } + + /* + THE ERROR, not a guess at it. + + This said "the todo board is owner-only, and this account was refused" — true of the version in the + platform, where `/todos` was `kind: 'admin'`, and false here, where the permission is grantable. It + survived the extraction as a sentence describing a rule that no longer exists, and the first real + failure it met was a 500 from a mis-pointed project, which it reported as a permission problem. + + A wrong explanation is worse than none: it sends somebody to check their account when the answer is + on screen in the panel beside them. + */ + if (error) { + return ( +
+ This board could not be read. + {errorText(error, 'unknown error')}
); } if (isLoading || !data) { - return
Reading todo/…
; + return
Reading…
; + } + + /* + No themes at all is almost always the same mistake — the project points at a repository rather than + at the folder inside it that holds the themes. Said here, where somebody is looking, rather than left + as an empty board that reads like "no work outstanding". + */ + if (data.themes.length === 0) { + return ( +
+ No themes in this folder. + + A theme is a directory with an _index.md in it. If you pointed this at a repository, the folder + you want is usually todo/ inside it. + +
+ ); } const themes = data.themes.filter((t) => t.items.length > 0);