Skip to content

feat(dashboard): retrospective links for done tasks + time-based filter#95

Merged
lukstafi merged 2 commits intomainfrom
ludics/task-322291a6-s5/root
Mar 28, 2026
Merged

feat(dashboard): retrospective links for done tasks + time-based filter#95
lukstafi merged 2 commits intomainfrom
ludics/task-322291a6-s5/root

Conversation

@lukstafi
Copy link
Copy Markdown
Owner

Summary

  • Done tasks with a retrospective JSON file now show a retrospective link in the task row (tasks.html → retroLink field via TasksTreeNode)
  • A filter bar (All / Last 30 days / Last 7 days / Last 24 hours) appears above the tasks forest; filter state persists in localStorage
  • Filter uses file mtime; recursive filterTree() keeps project nodes only when descendants pass; shows "No tasks match" placeholder when empty

Backend (src/dashboard.ts)

  • DashboardTask: added mtime: string | null, hasRetrospective: boolean
  • TasksTreeNode: added retroLink: string | null, mtime: string | null
  • readDashboardTasks(): reads statSync mtime and existsSync retro JSON per task
  • buildTaskNode(): propagates mtime/retroLink; fallback + project nodes get null

Frontend (templates/dashboard/tasks.html)

  • Filter bar HTML + CSS (.filter-bar, .filter-btn, .active)
  • filterCutoff() + filterTree() for recursive mtime-based pruning
  • renderNode() renders retroLink anchor after proposal link
  • No new API endpoints; all data flows through existing tasks-tree.json

Test plan

  • Typecheck passes (bun run typecheck — confirmed clean)
  • Tasks with retrospectives/<id>.json show a "retrospective" link in the task row
  • Tasks without a retro file show no extra link
  • Filter buttons highlight correctly and re-render immediately from cached data
  • Filter selection survives page reload (localStorage)
  • "No tasks match the current filter" placeholder appears when filter excludes all tasks

🤖 Generated with Claude Code

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 316f2fced2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

};
let openNodeKeys = new Set();
let hasRendered = false;
let currentFilter = localStorage.getItem('tasks-filter') || 'all';
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Guard localStorage access for task filter state

Accessing localStorage at module load can throw a SecurityError in privacy-restricted contexts (for example, browsers with storage disabled), which aborts the script before DOMContentLoaded and prevents the task tree from rendering at all. This change introduces unconditional reads/writes (getItem/setItem) without a fallback path, so the Tasks page can become unusable for those users; wrapping storage access in try/catch with an in-memory default avoids the hard failure.

Useful? React with 👍 / 👎.

lukstafi and others added 2 commits March 28, 2026 16:57
- Add `mtime` and `hasRetrospective` to DashboardTask, propagate through TasksTreeNode
- Emit `retroLink` pointing to retrospective.html?task=ID for done tasks with a retro JSON
- Add filter bar (All / Last 30d / 7d / 24h) above tasks forest; state persists in localStorage
- Recursive filterTree() prunes project/task nodes by file mtime; shows placeholder when empty

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Accessing localStorage at module load can throw SecurityError in
privacy-restricted contexts, breaking the entire Tasks page. Wrap
getItem at init and setItem on filter change in try/catch so the
page degrades gracefully with an in-memory default when storage
is unavailable.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@lukstafi lukstafi force-pushed the ludics/task-322291a6-s5/root branch from e96edf7 to 7392e81 Compare March 28, 2026 15:57
@lukstafi lukstafi merged commit 1572f0c into main Mar 28, 2026
@lukstafi
Copy link
Copy Markdown
Owner Author

Refactor Notes — task-322291a6 (coder perspective)

What I'd do differently next time

1. Batch the statSync + existsSync calls into a single pass

Currently readDashboardTasks() calls statSync and existsSync per task file inside the loop. Both are synchronous syscalls. If the tasks directory ever grows large, this adds noticeable latency to dashboard generation. A cleaner approach would be to pre-scan the retrospectives/ directory once into a Set<string> before the loop, then do a single Set.has(id) check per task:

const retroIds = new Set(
  existsSync(retroDir)
    ? readdirSync(retroDir).filter(f => f.endsWith('.json')).map(f => f.slice(0, -5))
    : []
);
// then in the loop:
const hasRetrospective = isCompleted && retroIds.has(id);

This reduces O(n) existsSync calls to one readdirSync + O(1) lookups.

2. Derive retroLink on the frontend, not the backend

The retroLink field is just a URL template applied to id — no backend logic is needed. Adding it as a computed field in buildTaskNode() means the TasksTreeNode interface carries a redundant derived field. A cleaner separation would be: backend emits hasRetrospective: boolean, frontend computes retroLink in renderNode():

if (node.hasRetrospective) {
    retroLink.href = `retrospective.html?task=${encodeURIComponent(node.id)}`;
}

This keeps the interface leaner and avoids encoding/URL logic in TypeScript that belongs in HTML.

3. Move filter logic out of inline script into a small module

The filterTree and filterCutoff functions in tasks.html are pure and testable. Embedding them in a <script> block makes them invisible to tsc. Moving shared JS to templates/dashboard/filter.js (or a shared utils.js) and loading it with <script src="filter.js"> would allow future type-checking and reuse across dashboard pages (e.g. if index.html ever needs the same mtime filtering).

4. Expose mtime on project nodes by aggregating children

Project nodes currently carry mtime: null. This means the filter has to recurse into every project node's children even when a top-level cutoff could prune the whole subtree early. Propagating mtime as the max child mtime onto the project node would allow short-circuit pruning at the project level:

const projectMtime = childResults.reduce<string | null>((max, c) => {
  if (!c.node.mtime) return max;
  if (!max) return c.node.mtime;
  return c.node.mtime > max ? c.node.mtime : max;
}, null);

For typical usage this is a micro-optimisation, but it also makes the data model more consistent (every node has an mtime) and simplifies filterTree.

5. Initialise hasRendered = true only after a non-empty render

Currently hasRendered is set to true after any call to renderTasksTree that reaches the render loop — including filtered renders that might show only a subset of nodes. If the filter is changed before the first full render, some project nodes might not auto-open on first real load. Setting hasRendered only after an unfiltered render (or always after the first successful fetch) would be more correct:

if (!hasRendered && currentFilter === 'all') hasRendered = true;

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant