Skip to content

Develop#523

Merged
fccview merged 52 commits into
mainfrom
develop
Jun 14, 2026
Merged

Develop#523
fccview merged 52 commits into
mainfrom
develop

Conversation

@fccview

@fccview fccview commented May 27, 2026

Copy link
Copy Markdown
Owner

Changelog

features

  • Start work to deprecate dnd-kit and have our own lighter internal drag and drop engine
  • Keep the engine generic enough to allow it to be used for pinned items/checklists/sidebar
  • Migrate the entirety of Kanban to use said engine
  • Update styling of Kanban item modal to look a bit less chaotic
  • Update Kanban item modal to grow full width/height so there's more space to work with
  • Add Start Date to tasks so calendar view allows tasks to expand from start to end date (bit like google calendar)
  • Improve neural network on the settings page, from a hardcoded vector style link system to a full blown obsidian-like view of the nodes, it also uses tags now for linking (you can disable the checkbox if needed). I figured I'd push it, I worked on it a while ago and for the life of me I don't know why I didn't just go for it, it's hidden in the settings, doesn't touch anything else and I feel like it's a nice feature to have, so even if it's a bit buggy we can fix forward ❤️
  • Add "Archive All" option to Kanban columns that have autocomplete enabled - Thank you @reniko
  • Add consistent scrollbar across all browser/OS - Thank you @reniko
  • Add remember me toggle to login page [FEATURE]: Add a remember me checkbox at the authentication page #516

bugfixes

new endpoints

A community member decided to make an android client for Jotty.
It's very much under development and needs polishing but the author seems quite dedicated and I kept an eye on development.

They needed some endpoints for it to work better, so what better moment than this to give them a shoutout!
Check it out here if you are interested, I am not involved in the development, I can't vouch for it, but for what I've seen so far the author (@Darknetzz) is pretty dedicated! Link here: https://github.com/Darknetzz/jotty-android

New endpoints curl exmaples
# Search notes and checklists
curl -H "x-api-key: YOUR_API_KEY" "https://your-jotty.com/api/search?q=hello"

# Search filtered to notes only
curl -H "x-api-key: YOUR_API_KEY" "https://your-jotty.com/api/search?q=hello&type=note"

# Search filtered to checklists only
curl -H "x-api-key: YOUR_API_KEY" "https://your-jotty.com/api/search?q=hello&type=checklist"

# Update a checklist item text (PATCH)
curl -X PATCH \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"text": "Updated item text"}' \
  "https://your-jotty.com/api/checklists/LIST_UUID/items/0"

# Update a nested checklist item (dot-notation index)
curl -X PATCH \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"text": "Updated nested item", "description": "some notes"}' \
  "https://your-jotty.com/api/checklists/LIST_UUID/items/0.1"

# Reorder items - move item before another
curl -X PUT \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"activeItemId": "ITEM_ID_TO_MOVE", "overItemId": "TARGET_ITEM_ID", "position": "before"}' \
  "https://your-jotty.com/api/checklists/LIST_UUID/items/reorder"

# Reorder items - move item after another
curl -X PUT \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"activeItemId": "ITEM_ID_TO_MOVE", "overItemId": "TARGET_ITEM_ID", "position": "after"}' \
  "https://your-jotty.com/api/checklists/LIST_UUID/items/reorder"

# Reorder items - nest item as child of another
curl -X PUT \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"activeItemId": "ITEM_ID_TO_MOVE", "overItemId": "PARENT_ITEM_ID", "isDropInto": true}' \
  "https://your-jotty.com/api/checklists/LIST_UUID/items/reorder"

# Get all checklists with expanded item fields
curl -H "x-api-key: YOUR_API_KEY" \
  "https://your-jotty.com/api/checklists"

# Get all task/Kanban boards with expanded item fields
curl -H "x-api-key: YOUR_API_KEY" \
  "https://your-jotty.com/api/tasks"

# Get one task/Kanban board with expanded item fields
curl -H "x-api-key: YOUR_API_KEY" \
  "https://your-jotty.com/api/tasks/TASK_UUID"

# Get one task/Kanban item
curl -H "x-api-key: YOUR_API_KEY" \
  "https://your-jotty.com/api/tasks/TASK_UUID/items/0"

# Get one nested task/Kanban item (dot-notation index)
curl -H "x-api-key: YOUR_API_KEY" \
  "https://your-jotty.com/api/tasks/TASK_UUID/items/0.1"

# Update checklist/Kanban item rich fields
curl -X PATCH \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"description": "Additional notes", "priority": "high", "score": 5, "startDate": "2026-06-10", "targetDate": "2026-06-15", "estimatedTime": 2.5}' \
  "https://your-jotty.com/api/checklists/LIST_UUID/items/0"

# Update checklist/Kanban item text and notes
curl -X PATCH \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"text": "Updated item text", "description": "Updated notes"}' \
  "https://your-jotty.com/api/checklists/LIST_UUID/items/0"

# Update nested checklist/Kanban item rich fields (dot-notation index)
curl -X PATCH \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"priority": "critical", "score": 8, "startDate": "2026-06-13", "targetDate": "2026-06-20"}' \
  "https://your-jotty.com/api/checklists/LIST_UUID/items/0.1"

# Clear optional checklist/Kanban item fields
curl -X PATCH \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"description": null, "priority": null, "score": null, "startDate": null, "targetDate": null, "estimatedTime": null}' \
  "https://your-jotty.com/api/checklists/LIST_UUID/items/0"

Summary by CodeRabbit

  • New Features
    • Search (notes/checklists), interactive Connections Graph, server-backed kanban drag-and-drop, week-spanning calendar priority bars, and new task item APIs (GET/updates).
  • Improvements
    • Kanban: per-column “archive all”, status + start-date editing, mobile status dropdown preference, and enlarge-capable modals; “remember me” login; Mermaid sankey normalization; scrollbar/UI polish; improved mention suggestions.
  • Tests
    • Added/extended unit and integration tests for DnD, board logic, graph data, parsing, and drop/update actions.

reniko and others added 30 commits May 18, 2026 16:25
…omium

Replace the old webkit-only 8px scrollbar block with a unified approach:
- @supports (-moz-appearance: none) guard so Firefox-only scrollbar-width/color
  rules don't trigger Chrome 121+ standard scrollbar API
- Unified 6px webkit pseudo-element rules for .overflow-y-auto, .overflow-x-auto,
  .overflow-auto and .scrollbar-thin (no arrows, transparent track)
- Remove hide-scrollbar from Notes/Checklist overview and Sidebar so scrollbars
  are visible in both browsers
- Add tailwind-scrollbar plugin classes to KanbanCardDetail panes and Kanban board

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…change

Upstream added a bare <div> wrapper around Modal children in default size mode,
breaking the flex height chain. Target it via [&>div:last-child] to give it
flex-1 + flex-col so the inner panes get a defined height and can scroll again.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…efox

- Remove tailwind-scrollbar plugin classes from both scroll panes; the plugin
  sets scrollbar-width:thin unconditionally which triggers Chrome 121+ standard
  scrollbar API (auto-hide overlay) making scrollbars invisible in Edge
- Change lg:overflow-y-auto to overflow-y-auto so globals.css webkit rules
  (.overflow-y-auto::-webkit-scrollbar) apply, giving a always-visible 6px bar
- Add lg:[&>div:last-child]:overflow-hidden to prevent the Modal wrapper div
  from showing its own scrollbar on desktop, which overlapped in Firefox

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Tailwind arbitrary variants with pseudo-selectors ([&>div:last-child])
do not compile reliably. Replace with a dedicated CSS class:
.jotty-modal-content.jotty-kanban-detail-modal > div:last-child targets
the bare wrapper div upstream injected, making it flex-1 flex-col so the
inner layout and per-pane overflow-y-auto work again.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The upstream Modal wrapper div breaks the flex chain for default size.
size=fullscreen gives the children wrapper flex-1 overflow-auto by design.
Inner div uses min-h-full + lg:overflow-hidden so panes scroll independently
on desktop while the wrapper scrolls as one unit on mobile.
Removes the fragile CSS child selector approach.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
scrollbar-thin and related plugin classes set scrollbar-width on Chrome
121+, which causes it to ignore the ::-webkit-scrollbar rules in globals.css
and fall back to native auto-hide scrollbars. The global webkit rules already
cover .overflow-y-auto and .overflow-auto, so the plugin classes are
counterproductive on those elements.
The Firefox-only @supports block listed overflow-y-auto and overflow-auto
but missed overflow-x-auto, leaving horizontally scrolling containers with
the default OS scrollbar in Firefox. The corresponding webkit rules
already include overflow-x-auto.
Subitem drag-and-drop always inserted the dragged item before the drop
target instead of at the indicated position, so it landed at the top of
the parent's children.

The before/after check in handleDragEnd matched the drop-zone id prefix
"drop-after::", but subitem drop indicators use the prefix
"drop-after-child::". The check never matched for subitems, so position
always fell back to "before". Top-level reorder was unaffected because
its indicators do use "drop-after::".

Match the prefix "drop-after" so both top-level and subitem indicators
resolve correctly, consistent with the isDropIndicator check that
already uses that prefix.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
fix(checklist): respect drop position when reordering subitems
feat(kanban): add "Archive all" action to auto-complete columns
fix(scrollbar): consistent scrollbar styling across Firefox and Chromium browsers (Windows)
The client-side parseChecklistContent generated fallback item IDs as
`${id}-${currentItemIndex}` and reset currentItemIndex to 0 in each
recursive call for nested children. Group headers and their nested
items therefore shared the same numeric range, producing duplicate IDs
across siblings and groups. Clicking one item toggled every other item
with the same ID.

The server-side parser (parseMarkdown in checklist-utils.ts) already
avoided this by using a monotonic counter combined with level
(`${id}-${level}-${counter}`). Align the client parser with the same
scheme so a file dropped into the importer is assigned the same IDs
that the server would produce.

Also add a post-parse dedup pass so already-imported files with broken
duplicate IDs heal themselves on the next save: the first occurrence
keeps its ID, every later collision is reassigned a fresh unique ID.
Stored metadata IDs are still honoured.

Tests cover a 4-group, 5-children-each fresh parse (all IDs unique),
preservation of stored item-metadata IDs, and dedup of pre-broken
imported content.
fix: ensure unique item IDs when importing checklists (#501)
(feat) add Kanban status actions to card detail and overflow menu
@fccview

fccview commented Jun 13, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai do your thing

@coderabbitai

coderabbitai Bot commented Jun 13, 2026

Copy link
Copy Markdown

@fccview Sure, I'll kick off a full review of this PR right away! 🐇

✅ Action performed

Full review finished.

@coderabbitai

coderabbitai Bot commented Jun 13, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 4f4d5ae4-65d5-457c-ad5a-38a4d3ae0cd7

📥 Commits

Reviewing files that changed from the base of the PR and between e732198 and 3307091.

📒 Files selected for processing (2)
  • app/api/checklists/[listId]/items/[itemIndex]/route.ts
  • howto/API.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • howto/API.md

📝 Walkthrough

Walkthrough

This PR updates Kanban drag-and-drop, detail editing, calendar rendering, and bulk archive flows; adds a profile connections graph with data builders and filters; expands checklist/task API item shapes and routes; introduces remember-me, navigation-guard, and modal behavior changes; and updates translations, configs, and tests across the app.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant KanbanCard
  participant DndProvider
  participant useKanbanDnd
  participant dropItem
  participant ChecklistFile

  User->>KanbanCard: start drag
  KanbanCard->>DndProvider: begin(session)
  DndProvider->>useKanbanDnd: handleDrop(DropResult)
  useKanbanDnd->>dropItem: FormData(uuid,itemId,targetStatus,targetIndex)
  dropItem->>ChecklistFile: write updated checklist markdown
  dropItem->>useKanbanDnd: updated checklist result
  useKanbanDnd->>DndProvider: settle drag state
Loading
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch develop

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
app/(loggedInRoutes)/settings/connections/page.tsx (1)

19-23: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Archived items still leak back into the graph data.

Lines 19-23 only strip archived IDs out of linkIndex. LinksTab still receives the full notes and checklists arrays, and buildConnectionGraphData() builds both nodes and tag links from those arrays, so archived content can reappear through showOrphans or any shared tag. Filter those collections with the same archived set before passing them down, or move the archived-item check into the graph builder so the contract lives in one place.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/`(loggedInRoutes)/settings/connections/page.tsx around lines 19 - 23, The
LinksTab is still receiving unfiltered notes and checklists so archived items
leak back into the graph; update the return to pass filtered notes and
checklists (using the same archivedItems set used by filterArchivedLinkIndex) —
e.g. create filteredNotes = notes.filter(n => !archivedItemsSet.has(n.id)) and
filteredChecklists = checklists.filter(c => !archivedItemsSet.has(c.id)) (or
move the archived check into buildConnectionGraphData so it rejects archived
nodes/tags there), then pass filteredNotes and filteredChecklists into LinksTab
instead of the original notes/checklists.
app/_components/FeatureComponents/Kanban/Kanban.tsx (1)

275-302: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don't archive items hidden by active filters.

This column renders items after search/priority/assignee filtering, but the bulk action still archives columnItems. With any filter active, "archive all" persists changes to cards the user cannot see.

🛡️ Safer wiring
           const columnItems = getItemsByStatus(column.status);
           const items = _filterItems(columnItems);
@@
-                archivableCount={columnItems.length}
+                archivableCount={items.length}
                 onArchiveAll={
                   permissions?.canEdit
-                    ? () => handleArchiveAll(columnItems)
+                    ? () => handleArchiveAll(items)
                     : undefined
                 }

If the intended behavior is "archive the full status column regardless of filters", then disable or hide this action whenever filters are active instead of silently acting on hidden rows.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/_components/FeatureComponents/Kanban/Kanban.tsx` around lines 275 - 302,
The "Archive all" handler is acting on unfiltered columnItems while the UI shows
filtered items (items), causing hidden cards to be archived; change the
onArchiveAll wiring on the KanbanColumn so it either (A) archives only the
currently-visible filtered items by calling handleArchiveAll(items) instead of
handleArchiveAll(columnItems), or (B) disables/hides the archive-all action when
filters are active by detecting a filter state (e.g., compare items.length !==
columnItems.length or use an existing filter flag) and passing undefined for
onArchiveAll when filtered (permissions?.canEdit ? () => handleArchiveAll(items)
: undefined OR permissions?.canEdit && !filtersActive ? () =>
handleArchiveAll(columnItems) : undefined). Ensure use of the existing symbols:
columnItems, items, _filterItems, handleArchiveAll, and permissions?.canEdit.
app/_hooks/kanban/useKanbanItem.tsx (1)

57-70: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don't resume the timer across time spent with the page closed.

This restore path only persists startTime, then resumes by computing Date.now() - storedStart. After a reload or browser close, the timer keeps accruing the offline gap as active work time, so the next saved entry is inflated.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/_hooks/kanban/useKanbanItem.tsx` around lines 57 - 70, The restore logic
in useEffect (TIMER_STORAGE_KEY, JSON.parse(stored)) resumes timers using
storedStart which counts offline time; instead change the restore path so that
when stored.isRunning is true you do NOT compute Date.now() - storedStart: call
setStartTime(new Date()), setIsRunning(true) and setCurrentTime(0) (i.e., start
fresh from now) so offline time is not treated as active work; keep the existing
try/catch and handling for non-running state unchanged.
🟠 Major comments (23)
app/_components/FeatureComponents/Profile/Parts/ConnectionsGraph/graph-data.ts-200-220 (1)

200-220: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Shared tags currently expand into a full clique.

Lines 200-220 create one edge for every pair of items that shares a tag. At the current 600-node ceiling, one popular tag can still emit ~180k links, which will make both buildConnectionGraphData() and the force-graph render stall badly on the client. Consider capping per-tag fan-out or representing tags as intermediary nodes instead of pairwise edges.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@app/_components/FeatureComponents/Profile/Parts/ConnectionsGraph/graph-data.ts`
around lines 200 - 220, This code emits a full clique for each tag (in
nodesByTag loop) which explodes links; fix by replacing the pairwise link
creation in this block with either (A) a tag-as-intermediary strategy: create a
synthetic node for the tag (add a node object for the tag and a single set of
links from that tag-node to each member, e.g. set link.type = "tag-node" and
mark directed=false) and update tagConnections for members accordingly, and
ensure buildConnectionGraphData()/any node-assembly logic includes these tag
nodes; or (B) a capped-fanout strategy: limit taggedNodeIds before pairing (e.g.
sample or take first MAX_PER_TAG) and then emit pairwise links only for that
reduced set; implement one of these options inside the nodesByTag.forEach block
(referencing nodesByTag, linkMap, tagConnections) and update
buildConnectionGraphData to handle the new tag-node type if you choose the
intermediary approach.
app/_translations/fr.json-287-287 (1)

287-287: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Incomplete translation: "rememberMe" is in English instead of French.

The value "Remember me" should be translated to French (e.g., "Se souvenir de moi" or "Rester connecté"). French-speaking users will see English text on the login screen.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/_translations/fr.json` at line 287, The "rememberMe" translation key
currently has an English value; update the JSON value for the "rememberMe" key
to a proper French translation (for example "Se souvenir de moi" or "Rester
connecté") so the login UI displays French text for that label.
app/_translations/it.json-287-287 (1)

287-287: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Incomplete translation: "rememberMe" is in English instead of Italian.

The value "Remember me" should be translated to Italian (e.g., "Ricordami" or "Resta connesso"). Italian-speaking users will see English text on the login screen.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/_translations/it.json` at line 287, The translation for the "rememberMe"
key is still in English; update the value for the "rememberMe" JSON key to the
correct Italian translation (for example "Ricordami" or "Resta connesso") so the
login UI shows Italian text for that label.
app/_translations/fr.json-696-713 (1)

696-713: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Incomplete translations for connections graph UI across French and Italian locales.

Both translation files contain the same 17 untranslated English strings for the connections graph feature. This systematic gap suggests the graph feature was implemented without completing the localization work, resulting in a degraded user experience for non-English speakers.

  • app/_translations/fr.json#L696-L713: Translate all graph-related keys to French (e.g., "totalConnections" → "Total", "inboundConnections" → "Entrant", "outboundConnections" → "Sortant", "orphans" → "Orphelins", "labels" → "Étiquettes", "arrows" → "Flèches", "nodeSize" → "Taille des nœuds", "linkWidth" → "Largeur des liens", "linkDistance" → "Distance des liens", "tagLinks" → "Liens de tags", etc.).
  • app/_translations/it.json#L696-L713: Translate all graph-related keys to Italian (e.g., "totalConnections" → "Totale", "inboundConnections" → "In entrata", "outboundConnections" → "In uscita", "orphans" → "Orfani", "labels" → "Etichette", "arrows" → "Frecce", "nodeSize" → "Dimensione nodi", "linkWidth" → "Larghezza collegamenti", "linkDistance" → "Distanza collegamenti", "tagLinks" → "Collegamenti tag", etc.).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/_translations/fr.json` around lines 696 - 713, The French and Italian
translation files both contain untranslated English graph UI strings; update
app/_translations/fr.json (lines 696-713) by translating each key (e.g.,
"totalConnections","inboundConnections","outboundConnections","orphans","labels","arrows","nodeSize","linkWidth","linkDistance","tagLinks","visibleGraphSummary","graphTruncated","updated","openItem","selectItem","inspectNodeHint","graphSearchPlaceholder")
into idiomatic French (examples:
"Total","Entrant","Sortant","Orphelins","Étiquettes","Flèches","Taille des
nœuds","Largeur des liens","Distance des liens","Liens de tags", etc.); and
update app/_translations/it.json (lines 696-713) by translating the same keys
into Italian (examples: "Totale","In entrata","In
uscita","Orfani","Etichette","Frecce","Dimensione nodi","Larghezza
collegamenti","Distanza collegamenti","Collegamenti tag", etc.), ensuring
placeholders like {items},{links},{visible},{omitted} and UI hints remain intact
and grammatically correct.
app/_components/GlobalComponents/Auth/LoginForm.tsx-248-253 (1)

248-253: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Harden the external releases link against opener-based tabnabbing.

The target="_blank" anchor should include rel="noopener noreferrer" so the opened tab cannot access window.opener.

Proposed fix
           <a
             target="_blank"
+            rel="noopener noreferrer"
             href={`https://github.com/fccview/jotty/releases/tag/${appVersion}`}
           >
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/_components/GlobalComponents/Auth/LoginForm.tsx` around lines 248 - 253,
The external releases anchor in LoginForm.tsx (the <a> element rendering the
releases link inside the LoginForm component) uses target="_blank" without rel
attributes; update that anchor to add rel="noopener noreferrer" so the new tab
cannot access window.opener (preventing tabnabbing) while keeping the existing
href and target values unchanged.
app/_providers/NavigationGuardProvider.tsx-63-72 (1)

63-72: ⚠️ Potential issue | 🟠 Major

Add e.returnValue to make the beforeunload prompt reliable

File: app/_providers/NavigationGuardProvider.tsx
Lines: 63-72

useEffect(() => {
  const onBeforeUnload = (e: BeforeUnloadEvent) => {
    if (hasUnsavedChanges) {
      e.preventDefault();
    }
  };

  window.addEventListener("beforeunload", onBeforeUnload);
  return () => window.removeEventListener("beforeunload", onBeforeUnload);
}, [hasUnsavedChanges]);

beforeunload may not trigger consistently with preventDefault() alone. Set e.returnValue when blocking unload to ensure cross-browser behavior.

Proposed fix
   useEffect(() => {
     const onBeforeUnload = (e: BeforeUnloadEvent) => {
       if (hasUnsavedChanges) {
         e.preventDefault();
+        e.returnValue = "";
       }
     };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/_providers/NavigationGuardProvider.tsx` around lines 63 - 72, The
beforeunload handler (onBeforeUnload used inside the useEffect) currently calls
e.preventDefault() when hasUnsavedChanges is true but doesn't set e.returnValue,
which can make the prompt unreliable across browsers; update the handler so that
when hasUnsavedChanges is true you both call e.preventDefault() and set
e.returnValue = '' (or a short string) on the BeforeUnloadEvent before
returning, ensuring consistent cross-browser blocking behavior while keeping the
existing window.addEventListener/removeEventListener logic intact.
app/_components/FeatureComponents/Notes/Parts/TipTap/CustomExtensions/ExcalidrawExtension.tsx-109-115 (1)

109-115: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Prevent close-path lock when autosave fails.

Line 111 starts an async save without handling rejection in the close path. If save/export fails, close can become non-functional and trap the modal open.

Suggested patch
-  const handleClose = () => {
-    if (excalidrawAPI) {
-      handleSave();
-    } else {
-      setIsEditing(false);
-    }
-  };
+  const handleClose = async () => {
+    if (!excalidrawAPI) {
+      setIsEditing(false);
+      return;
+    }
+
+    try {
+      await handleSave();
+    } catch {
+      setIsEditing(false);
+    }
+  };
...
-        onClose={handleClose}
+        onClose={() => {
+          void handleClose();
+        }}

Also applies to: 140-140

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@app/_components/FeatureComponents/Notes/Parts/TipTap/CustomExtensions/ExcalidrawExtension.tsx`
around lines 109 - 115, handleClose currently triggers the async handleSave when
excalidrawAPI exists without handling rejection, which can lock the close path;
make handleClose robust by ensuring it always proceeds to close regardless of
save success: either make handleClose async and await handleSave() inside a
try/catch/finally that calls setIsEditing(false) in the finally block, or call
handleSave().catch(() => {/* optional log */}).finally(() =>
setIsEditing(false)); apply the same fix to the other similar close-handler
occurrence in this file (the second handleClose-like close path).
app/_utils/dnd/rect-registry.ts-108-117 (1)

108-117: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Include the list node itself in scrollables() traversal.

Line 111 starts at el.parentElement, so a scrollable drop-list element is never added. That can block drag auto-scroll within long lists.

Proposed fix
-      lists.forEach(({ el }) => {
-        let node = el.parentElement;
+      lists.forEach(({ el }) => {
+        let node: HTMLElement | null = el;
         while (node) {
           if (_scrolls(node)) found.add(node);
           node = node.parentElement;
         }
       });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/_utils/dnd/rect-registry.ts` around lines 108 - 117, The scrollables()
traversal in rect-registry.ts currently starts from el.parentElement so
scrollable list elements themselves are skipped; within the scrollables()
function (which iterates lists and uses _scrolls(node)), start traversal from
the list node itself (let node = el) instead of el.parentElement so that
_scrolls(el) can detect and add the list element to the found set (leave the
rest of the while loop logic unchanged).
app/_components/FeatureComponents/Kanban/CalendarView.tsx-155-172 (1)

155-172: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Make the calendar bars keyboard-accessible.

The new event bars are clickable divs, so keyboard users can't focus or activate them. Opening a task from calendar view becomes mouse-only.

♿ Suggested fix
-                    <div
+                    <button
+                      type="button"
                       key={`${segment.event.id}-${segment.colStart}-${segment.lane}`}
                       onClick={() => _openItem(segment.event.itemId)}
+                      aria-label={segment.event.title}
+                      title={segment.event.title}
                       className={cn(
-                        "pointer-events-auto h-4 text-[10px] font-medium leading-4 px-1.5 truncate cursor-pointer hover:brightness-95 dark:hover:brightness-110 transition-[filter,opacity]",
+                        "pointer-events-auto h-4 text-left text-[10px] font-medium leading-4 px-1.5 truncate cursor-pointer hover:brightness-95 dark:hover:brightness-110 transition-[filter,opacity] appearance-none border-0",
                         !barStyle.backgroundColor && "bg-muted/80 text-muted-foreground",
                         segment.continuesPrev ? "rounded-l-none ml-0" : "rounded-l-jotty ml-0.5",
                         segment.continuesNext ? "rounded-r-none mr-0" : "rounded-r-jotty mr-0.5",
                         segment.event.completed && "line-through opacity-70",
                       )}
                       style={{
                         ...barStyle,
                         gridColumn: `${segment.colStart + 1} / span ${segment.colSpan}`,
                         gridRow: segment.lane + 1,
                       }}
                     >
                       {!segment.continuesPrev ? segment.event.title : "\u00a0"}
-                    </div>
+                    </button>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/_components/FeatureComponents/Kanban/CalendarView.tsx` around lines 155 -
172, The event bars are clickable divs (the element created in CalendarView that
uses onClick={() => _openItem(segment.event.itemId)}) and must be
keyboard-accessible: change the div to remain a div but add tabIndex={0},
role="button", a descriptive aria-label (e.g., using segment.event.title), and
implement an onKeyDown handler that calls _openItem(segment.event.itemId) when
Enter or Space is pressed; keep the existing onClick and visual classes
unchanged so mouse behavior is preserved and screen readers receive the label.
public/api/paths/checklists.yaml-201-251 (1)

201-251: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Merge patch and delete under a single /checklists/{listId}/items/{itemIndex} path item.

This path key is declared twice. YAML/OpenAPI parsers typically keep only the last mapping entry, so one operation can disappear from the generated spec.

#!/bin/bash
rg -n '^/checklists/\{listId\}/items/\{itemIndex\}:' public/api/paths/checklists.yaml

Expected result after the fix: exactly one match, with both patch: and delete: nested beneath that single path key.

Also applies to: 390-390

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@public/api/paths/checklists.yaml` around lines 201 - 251, The YAML defines
the same path key '/checklists/{listId}/items/{itemIndex}' twice so one
operation is lost; locate both occurrences and merge them into a single path
mapping that contains both the patch and delete operations (keep the existing
patch: block shown and add the delete: block under the same path key, or vice
versa), removing the duplicate top-level path entry so the file contains exactly
one '/checklists/{listId}/items/{itemIndex}:' mapping with both patch and delete
nested beneath it.
app/_utils/item-status-utils.ts-50-77 (1)

50-77: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Propagate auto-complete through all ancestors, not just the immediate parent.

completeParent() only evaluates the first parent returned by _findParent(). In a deeper tree, completing a grandchild can auto-complete its direct parent but never re-check the grandparent, so nested completion stops one level too early.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/_utils/item-status-utils.ts` around lines 50 - 77, completeParent
currently only checks the immediate parent returned by _findParent and stops;
change it so auto-complete is propagated up the ancestor chain: after finding
the parent (using _findParent) and confirming all its children are completed and
an autoCompleteStatus exists, update that parent via updateItem as you do now,
then loop/recursively set childId = parent.id and repeat the same logic (find
its parent, check siblings' completion, apply update) until no parent or no
autoCompleteStatus or not all siblings completed. Ensure you preserve and append
history/lastModified fields on each ancestor update and avoid infinite loops by
stopping when parent is null or no change is needed.
app/api/checklists/[listId]/items/reorder/route.ts-97-106 (1)

97-106: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Publish reorder writes through the same invalidation path as the other checklist mutations.

This endpoint persists the markdown file and returns success, but it never revalidates checklist pages or broadcasts the change. API-driven reorders will stay stale in other sessions until something else refreshes the list.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/api/checklists/`[listId]/items/reorder/route.ts around lines 97 - 106,
The reorder endpoint currently persists the markdown via stampOrder,
serverWriteFile and listToMarkdown but does not trigger the same
revalidation/broadcast used by other checklist mutations; after creating
updatedList and writing the file, call the centralized invalidation/publish
helper used by other checklist mutations (e.g., publishChecklistMutation or
whatever function other endpoints call to revalidate pages and broadcast
updates) with the updatedList (or list.id/owner/category) so the change
revalidates checklist pages and is broadcast to other sessions before returning
NextResponse.json({ success: true }); ensure you import or reference the exact
helper used by the other mutation handlers to avoid duplicating logic.
app/_server/actions/checklist-item/status.ts-74-90 (1)

74-90: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Invalid timeEntries should fail the action instead of being silently ignored.

If JSON.parse(timeEntriesStr) throws, this branch logs and returns the original item, but the action still rewrites the checklist, bumps updatedAt, broadcasts an update, and returns { success: true }. Callers will think the time entry was saved when it was actually dropped.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/_server/actions/checklist-item/status.ts` around lines 74 - 90, The code
silently swallows JSON.parse errors when building updatedItems (using
timeEntriesStr, updateItem, statusItems, itemId), allowing the action to proceed
as if the time entries were saved; change the catch to fail the action instead:
validate/parse timeEntriesStr before computing updatedItems (or rethrow inside
the updateItem callback) and when parsing fails throw a descriptive Error
(including the original exception) so the calling action aborts, does not
compute updatedItems, does not update updatedAt, does not broadcast, and returns
an error response rather than { success: true }.
app/_utils/item-status-utils.ts-63-75 (1)

63-75: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Make parent auto-complete idempotent before appending history.

This always writes a new history entry once all children are complete, even if the parent is already completed with the same auto-complete status. Because app/_server/actions/checklist-item/status.ts now calls this after any child update, pure time-entry edits on completed children will keep duplicating the parent's completion history.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/_utils/item-status-utils.ts` around lines 63 - 75, The current logic in
the update callback always appends a completion history entry when all children
are complete; change it to be idempotent by first checking the existing parent
state (p.completed and p.status) and only perform the update and append a
history entry if the parent is not already completed with the same
autoCompleteStatus.id. Concretely, inside the updateItem callback (the function
that receives p), if p.completed === true && p.status === autoCompleteStatus.id
then return p unchanged (or return p with no new history/modified fields);
otherwise set completed: true, status: autoCompleteStatus.id, lastModifiedBy:
username, lastModifiedAt: now and append the new history entry as currently
implemented.
app/api/search/route.ts-37-43 (1)

37-43: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Checklist search skips shared lists because it only scans the caller's directory.

Checklist files are stored under data/checklists/<owner> in the server write paths, but this route only searches data/checklists/<user.username>. After the shared-checklists API change, items from lists shared by other users will never be returned here.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/api/search/route.ts` around lines 37 - 43, The checklist search only
scans the caller's own directory (checklistsDir) so it misses lists shared by
other users; change the rawChecklists logic to build a list of checklist owner
directories (the caller's data/checklists/<user.username> plus every owner dir
returned by the shared-checklists API), then run grepSearchContent against each
directory and concatenate the results. Concretely: replace the single
checklistsDir used in the Promise.all with code that calls whatever
shared-checklist lookup you have (e.g., a function that returns shared owners
for user.username), map those owner names into path.join(process.cwd(), "data",
CHECKLISTS_FOLDER, owner), call grepSearchContent(...) for each directory
(handling errors with .catch(() => [])), and flatten the array into
rawChecklists so downstream code that expects grepSearchContent results
continues to work (update the rawChecklists construction where NOTES_DIR,
CHECKLISTS_FOLDER and grepSearchContent are referenced).
app/_server/actions/file/index.ts-91-97 (1)

91-97: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use a unique temp file per write; the shared .tmp path still races under concurrent requests.

finalPath + ".tmp" is the same file for every writer targeting that JSON path. Two overlapping writes can clobber each other's temp content or make one rename() fail with ENOENT, which reintroduces lost-update behavior for hot files like session data.

Suggested fix
-  const tmpPath = finalPath + ".tmp";
+  const tmpPath = `${finalPath}.${process.pid}.${Date.now()}.${Math.random()
+    .toString(36)
+    .slice(2)}.tmp`;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/_server/actions/file/index.ts` around lines 91 - 97, The current write
uses a shared tmpPath (tmpPath = finalPath + ".tmp") which races under
concurrent writers; change the temp-file strategy in the block that creates
tmpPath/uses fs.writeFile/fs.rename so each writer uses a unique temp name
(e.g., append process.pid + timestamp + random/UUID or use a per-write
crypto-random suffix), write to that unique tmpPath, then atomic-rename it to
finalPath; also ensure you unlink the unique tmpPath on errors (or handle rename
ENOENT appropriately) so leftover temps are cleaned up and concurrent writes no
longer clobber each other (references: variables finalPath, tmpPath and the
fs.writeFile/fs.rename operations).
app/_server/actions/checklist-item/drop.ts-25-31 (1)

25-31: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Validate targetStatus against allowed board statuses before applying the move.

Currently any string can be persisted to item.status, which can create values not present in list.statuses.

Suggested fix
+import { DEFAULT_KANBAN_STATUSES } from "`@/app/_consts/kanban`";
@@
     if (!list.items.some((item) => item.id === itemId)) {
       return { success: false, error: "Item not found" };
     }
+
+    const allowedStatuses =
+      list.statuses && list.statuses.length > 0
+        ? list.statuses
+        : DEFAULT_KANBAN_STATUSES;
+    if (!allowedStatuses.some((status) => status.id === targetStatus)) {
+      return { success: false, error: "Invalid targetStatus" };
+    }
 
     const now = new Date().toISOString();

Also applies to: 53-61

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/_server/actions/checklist-item/drop.ts` around lines 25 - 31, Validate
that targetStatus is one of the allowed statuses before accepting the input:
instead of only checking presence of uuid/itemId/targetStatus/targetIndex, fetch
or reference the corresponding list.statuses (the board's allowed statuses) and
ensure targetStatus is included; if not, return success: false with a clear
error. Update both validation sites (the existing check around
uuid/itemId/targetStatus/targetIndex and the similar check at the later block
noted by the reviewer) so they both reject unknown statuses rather than allowing
arbitrary strings to be persisted to item.status.
app/_server/actions/checklist-item/drop.ts-86-93 (1)

86-93: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Do not return a failed action after persistence just because broadcast fails.

At Lines 86-91, a websocket failure bubbles to the outer catch, returning "Failed to drop item" even though data is already saved. That desynchronizes client rollback behavior from persisted state.

Suggested fix
-    await broadcast({
-      type: "checklist",
-      action: "updated",
-      entityId: list.id,
-      username,
-    });
+    try {
+      await broadcast({
+        type: "checklist",
+        action: "updated",
+        entityId: list.id,
+        username,
+      });
+    } catch (error) {
+      console.warn("Broadcast failed after successful dropItem save:", error);
+    }
 
     return { success: true, data: updatedList };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/_server/actions/checklist-item/drop.ts` around lines 86 - 93, The
broadcast websocket call is allowed to fail and currently bubbles up to the
outer catch causing a false "Failed to drop item" after persistence; wrap the
broadcast(...) call in its own try/catch (or move the return { success: true,
data: updatedList } to before broadcasting) so persistence success is always
returned; on broadcast failure, log/report the error but do not rethrow it,
ensuring the function returns the persisted updatedList even if broadcast({
type: "checklist", action: "updated", entityId: list.id, username }) fails.
app/_hooks/kanban/useKanbanDnd.ts-74-89 (1)

74-89: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Handle thrown dropItem errors so optimistic state is always reverted.

At Line 74, dropItem(formData) is awaited without try/catch. If it throws, Lines 82-88 never run, leaving the optimistic state stuck.

Suggested fix
-      const response = await dropItem(formData);
-      if (response.success && response.data) {
-        setChecklist(response.data);
-        onUpdate(response.data);
-        return;
-      }
-
-      console.error("dropItem failed:", response.error);
-      setChecklist(snapshot);
-      onUpdate(snapshot);
-      showToast({
-        type: "error",
-        title: t("common.error"),
-        message: t("kanban.moveFailed"),
-      });
+      try {
+        const response = await dropItem(formData);
+        if (response.success && response.data) {
+          setChecklist(response.data);
+          onUpdate(response.data);
+          return;
+        }
+        throw new Error(response.error || "dropItem failed");
+      } catch (error) {
+        console.error("dropItem failed:", error);
+        setChecklist(snapshot);
+        onUpdate(snapshot);
+        showToast({
+          type: "error",
+          title: t("common.error"),
+          message: t("kanban.moveFailed"),
+        });
+      }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/_hooks/kanban/useKanbanDnd.ts` around lines 74 - 89, The await call to
dropItem(formData) in the drop handler may throw and leaves the optimistic
state; wrap the await in a try/catch so thrown errors also revert state: in the
try keep the existing behavior (await dropItem, if success
setChecklist(response.data), onUpdate(response.data) and return; otherwise fall
through to the existing failure block), and in the catch setChecklist(snapshot),
call onUpdate(snapshot), log the caught error (console.error) and call showToast
with the same error message so the optimistic state is always reverted on
exceptions.
app/_utils/kanban/board-utils.ts-38-41 (1)

38-41: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Clamp negative visIndex before anchor lookup.

At Line 38, -1 < visibleItems.length is true, so Line 39 can produce undefined and Line 40 throws on anchor.id.

Suggested fix
 export const visToColIndex = (
   visibleItems: Item[],
   columnItems: Item[],
   visIndex: number,
 ): number => {
-  if (visIndex < visibleItems.length) {
-    const anchor = visibleItems[visIndex];
+  const safeVisIndex = Math.max(0, visIndex);
+  if (safeVisIndex < visibleItems.length) {
+    const anchor = visibleItems[safeVisIndex];
     const anchorIdx = columnItems.findIndex((item) => item.id === anchor.id);
     return anchorIdx === -1 ? columnItems.length : anchorIdx;
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/_utils/kanban/board-utils.ts` around lines 38 - 41, The guard that uses
visIndex must also prevent negative indices: change the condition around the
anchor lookup so visIndex is clamped or checked to be >= 0 before accessing
visibleItems (e.g., ensure visIndex >= 0 && visIndex < visibleItems.length or
set visIndex = Math.max(0, visIndex) before using it); then proceed to compute
const anchor = visibleItems[visIndex], find anchorIdx in columnItems, and return
anchorIdx === -1 ? columnItems.length : anchorIdx (variables: visIndex,
visibleItems, anchor, anchorIdx, columnItems).
app/_utils/checklist-utils.ts-107-109 (1)

107-109: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Server-side parsing still preserves duplicate stored item IDs.

The new generator only helps when metadata.id is missing. If an existing checklist file already contains duplicate metadata.id values, parseMarkdown() still returns duplicate Item.ids even though app/_utils/client-parser-utils.ts now repairs them. All of the mutate-by-id paths (updateItem, delete, reorder) assume IDs are unique, so after a reload the wrong item can be mutated.

Suggested fix
   const { items } = buildNestedItems(itemLines, 0, 0, 0);
+
+  const dedupeItemIds = (itemList: Item[], seen: Set<string>): void => {
+    itemList.forEach((item) => {
+      if (seen.has(item.id)) {
+        item.id = generateItemId(0);
+      }
+      seen.add(item.id);
+      if (item.children?.length) {
+        dedupeItemIds(item.children, seen);
+      }
+    });
+  };
+
+  dedupeItemIds(items, new Set<string>());
 
   let statuses = undefined;

Also applies to: 230-230, 273-273

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/_utils/checklist-utils.ts` around lines 107 - 109, parseMarkdown
currently preserves duplicate metadata.id values so server-side parsed Item.id
can collide and break mutate-by-id paths (updateItem, delete, reorder); modify
parseMarkdown to detect duplicate IDs while building items and replace any
repeated metadata.id with a generated id (reuse generateItemId) so every
returned Item.id is unique, mirroring the repair logic in
app/_utils/client-parser-utils.ts; ensure the deduplication runs during parsing
(before returning items) and that generateItemId is available in that scope or
passed in, so downstream code can safely assume unique Item.id values.
app/api/checklists/[listId]/items/[itemIndex]/route.ts-19-27 (1)

19-27: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reject non-string text/description values before appending to FormData.

This route only checks presence. If a client sends { "text": {} } or { "description": [] }, FormData.append() will stringify it and updateItem() will persist [object Object] or a comma-joined array into the markdown file. Since this is a public API endpoint, the payload needs a type check before it reaches storage.

Suggested fix
       const body = await request.json();
       const { text, description } = body;
+
+      if (
+        (text !== undefined && typeof text !== "string") ||
+        (description !== undefined && typeof description !== "string")
+      ) {
+        return NextResponse.json(
+          { error: "'text' and 'description' must be strings" },
+          { status: 400 },
+        );
+      }

       if (!text && description === undefined) {
         return NextResponse.json(
           { error: "Provide at least 'text' or 'description' to update" },
           { status: 400 },
         );
       }
@@
-      if (text) formData.append("text", text);
-      if (description !== undefined) formData.append("description", description ?? "");
+      if (typeof text === "string" && text.length > 0) {
+        formData.append("text", text);
+      }
+      if (typeof description === "string") {
+        formData.append("description", description);
+      }

Also applies to: 61-62

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/api/checklists/`[listId]/items/[itemIndex]/route.ts around lines 19 - 27,
Validate that incoming text and description are strings before appending to
FormData or calling updateItem: if text is provided and typeof text !== "string"
or description is provided and typeof description !== "string", return
NextResponse.json with a 400 error (e.g. "text and description must be strings
when provided"). Apply this same check at the other update site that also
appends to FormData (the later block that handles partial updates) so non-string
values are rejected consistently before any FormData.append or updateItem(...)
calls.
app/_components/FeatureComponents/Kanban/KanbanCardDetail.tsx-343-369 (1)

343-369: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Keep date-only fields as date-only, and don’t wipe startDate when clearing targetDate.

These handlers currently round-trip a DatePicker value through new Date(value).toISOString() and also clear startDate in the !value branch. For a US user, selecting 2026-06-13 can re-render as 2026-06-12 after _toLocalDateValue() converts the stored UTC midnight back to local time, and removing only the target date will silently delete the saved start date as well.

Suggested fix
 const handleStartDateChange = async (value: string) => {
   setStartDateInput(value);
-  const iso = value ? new Date(value).toISOString() : "";
   const targetKey = _dateKey(targetDateInput);
   if (value && targetKey && value > targetKey) {
     setTargetDateInput(value);
-    await _saveField({ startDate: iso, targetDate: iso });
+    await _saveField({ startDate: value, targetDate: value });
     return;
   }
-  await _saveField({ startDate: iso });
+  await _saveField({ startDate: value });
 };

 const handleTargetDateChange = async (value: string) => {
   setTargetDateInput(value);
   if (!value) {
-    setStartDateInput("");
-    await _saveField({ targetDate: "", startDate: "" });
+    await _saveField({ targetDate: "" });
     return;
   }
-  const iso = new Date(value).toISOString();
   const startKey = _dateKey(startDateInput);
   if (startKey && startKey > value) {
     setStartDateInput(value);
-    await _saveField({ targetDate: iso, startDate: iso });
+    await _saveField({ targetDate: value, startDate: value });
     return;
   }
-  await _saveField({ targetDate: iso });
+  await _saveField({ targetDate: value });
 };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/_components/FeatureComponents/Kanban/KanbanCardDetail.tsx` around lines
343 - 369, The handlers handleStartDateChange and handleTargetDateChange
currently convert the date-picker value to an ISO UTC datetime and also clear
startDate when targetDate is cleared; change them to keep and persist date-only
strings (use the raw YYYY-MM-DD value from the picker instead of new
Date(...).toISOString()) and do not wipe startDate when targetDate is cleared.
Concretely: in handleStartDateChange and handleTargetDateChange use the incoming
value (not new Date(...).toISOString()) when calling _saveField, use
_dateKey(startDateInput)/_dateKey(targetDateInput) only for comparisons, and in
the !value branch of handleTargetDateChange only set targetDate to "" (do not
set startDate to ""). Ensure comparisons that enforce start<=target still update
both fields when needed.
🧹 Nitpick comments (3)
tests/server-actions/auth.test.ts (1)

346-351: ⚡ Quick win

Tighten the remember-me assertion to avoid false-positive test passes.

expect.any(Boolean) verifies only type, not behavior. This should assert false for this no-rememberMe input path, with a separate test asserting true when rememberMe is submitted.

Proposed fix for this test
       expect(mockCreateSession).toHaveBeenCalledWith(
         expect.any(String),
         'alice',
-        'ldap',
-        expect.any(Boolean)
+        'ldap',
+        false
       )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/server-actions/auth.test.ts` around lines 346 - 351, The test currently
uses expect.any(Boolean) for the remember-me flag which is too loose; update the
assertion on mockCreateSession to expect(false) for this no-rememberMe input
path (i.e., change expect.any(Boolean) to false in the
expect(mockCreateSession).toHaveBeenCalledWith(...) call) and add a separate
test that submits rememberMe and asserts
expect(mockCreateSession).toHaveBeenCalledWith(..., true) to cover the positive
case.
app/_components/FeatureComponents/Kanban/KanbanCard.tsx (1)

98-102: ⚡ Quick win

Include the timer-stop callback in the effect dependency list.

Line 102 only depends on isLifted; include stopTimerOnDrag to avoid stale closure risk.

Suggested patch
+  const { stopTimerOnDrag } = kanbanItemHook;
+
   useEffect(() => {
     if (isLifted) {
-      kanbanItemHook.stopTimerOnDrag();
+      stopTimerOnDrag();
     }
-  }, [isLifted]);
+  }, [isLifted, stopTimerOnDrag]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/_components/FeatureComponents/Kanban/KanbanCard.tsx` around lines 98 -
102, The effect in KanbanCard's useEffect currently only depends on isLifted
which can capture a stale closure of kanbanItemHook.stopTimerOnDrag; update the
dependency array to include kanbanItemHook.stopTimerOnDrag (or the
stopTimerOnDrag function reference) so the effect re-runs when that callback
changes, i.e., change the dependency list from [isLifted] to [isLifted,
kanbanItemHook.stopTimerOnDrag] (or destructure stopTimerOnDrag and use
[isLifted, stopTimerOnDrag]) to avoid stale closures.
tests/server-actions/drop-item.test.ts (1)

154-227: ⚡ Quick win

Add regression tests for invalid targetStatus and broadcast failure after write.

Current suite does not assert these two contract paths, which are high-risk for state consistency.

Suggested test additions
+  it("rejects an invalid targetStatus", async () => {
+    const result = await dropItem(
+      createFormData({
+        uuid: BOARD_UUID,
+        itemId: "task-1",
+        targetStatus: "not-a-real-status",
+        targetIndex: "0",
+      }),
+    );
+
+    expect(result.success).toBe(false);
+    expect(mockServerWriteFile).not.toHaveBeenCalled();
+    expect(mockBroadcast).not.toHaveBeenCalled();
+  });
+
+  it("still returns success when broadcast fails after write", async () => {
+    mockBroadcast.mockRejectedValue(new Error("ws down"));
+
+    const result = await dropItem(
+      createFormData({
+        uuid: BOARD_UUID,
+        itemId: "task-1",
+        targetStatus: "todo",
+        targetIndex: "0",
+      }),
+    );
+
+    expect(result.success).toBe(true);
+    expect(mockServerWriteFile).toHaveBeenCalledTimes(1);
+  });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/server-actions/drop-item.test.ts` around lines 154 - 227, Add two tests
to the dropItem test suite: (1) "rejects invalid targetStatus" — call
dropItem(createFormData({... targetStatus: "invalid", ...})) and assert
result.success is false, result.error indicates invalid status (or at least
non-success), and that mockServerWriteFile and mockBroadcast were not called;
(2) "handles broadcast failure after write" — arrange mockServerWriteFile to
resolve and mockBroadcast to throw/reject, call dropItem with a valid payload,
assert the final result reports failure (result.success false and an appropriate
error), assert mockServerWriteFile was called (write occurred) and mockBroadcast
was called (and failed), ensuring the test documents the contract that writes
are not rolled back on broadcast failure. Reference the existing helpers and
mocks: dropItem, createFormData, mockServerWriteFile, and mockBroadcast.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: a620a354-9b49-44d6-b3c1-49efcab1c7e9

📥 Commits

Reviewing files that changed from the base of the PR and between 3a7c22f and 999a421.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (91)
  • app/(loggedInRoutes)/settings/connections/page.tsx
  • app/_components/FeatureComponents/Home/Parts/ChecklistHome.tsx
  • app/_components/FeatureComponents/Home/Parts/NotesHome.tsx
  • app/_components/FeatureComponents/Kanban/ArchivedItemsModal.tsx
  • app/_components/FeatureComponents/Kanban/CalendarView.tsx
  • app/_components/FeatureComponents/Kanban/Kanban.tsx
  • app/_components/FeatureComponents/Kanban/KanbanCard.tsx
  • app/_components/FeatureComponents/Kanban/KanbanCardDetail.tsx
  • app/_components/FeatureComponents/Kanban/KanbanCardDetailProperties.tsx
  • app/_components/FeatureComponents/Kanban/KanbanColumn.tsx
  • app/_components/FeatureComponents/Kanban/KanbanItemContent.tsx
  • app/_components/FeatureComponents/Notes/Parts/MermaidRenderer.tsx
  • app/_components/FeatureComponents/Notes/Parts/TipTap/CustomExtensions/ExcalidrawExtension.tsx
  • app/_components/FeatureComponents/Notes/Parts/TipTap/CustomExtensions/MermaidExtension.tsx
  • app/_components/FeatureComponents/Notes/Parts/TipTap/CustomExtensions/SlashCommands.tsx
  • app/_components/FeatureComponents/Profile/Parts/ConnectionsGraph/ConnectionsGraph.tsx
  • app/_components/FeatureComponents/Profile/Parts/ConnectionsGraph/graph-data.ts
  • app/_components/FeatureComponents/Profile/Parts/LinksTab.tsx
  • app/_components/FeatureComponents/Profile/Parts/UserPreferencesTab.tsx
  • app/_components/GlobalComponents/Auth/LoginForm.tsx
  • app/_components/GlobalComponents/Dropdowns/Dropdown.tsx
  • app/_components/GlobalComponents/Modals/Modal.tsx
  • app/_components/GlobalComponents/Sidebar/SidebarWrapper.tsx
  • app/_consts/dnd.ts
  • app/_hooks/dnd/DndProvider.tsx
  • app/_hooks/dnd/index.tsx
  • app/_hooks/dnd/useDragItem.ts
  • app/_hooks/dnd/useDropList.ts
  • app/_hooks/kanban/useCalendarView.ts
  • app/_hooks/kanban/useKanban.ts
  • app/_hooks/kanban/useKanbanDnd.ts
  • app/_hooks/kanban/useKanbanItem.tsx
  • app/_hooks/useChecklist.tsx
  • app/_hooks/useNoteEditor.tsx
  • app/_providers/NavigationGuardProvider.tsx
  • app/_schemas/user-schemas.ts
  • app/_server/actions/auth/index.ts
  • app/_server/actions/checklist-item/crud.ts
  • app/_server/actions/checklist-item/drop.ts
  • app/_server/actions/checklist-item/index.ts
  • app/_server/actions/checklist-item/status.ts
  • app/_server/actions/file/index.ts
  • app/_server/actions/note/parsers.ts
  • app/_server/actions/session/index.ts
  • app/_server/actions/users/crud.ts
  • app/_styles/globals.css
  • app/_translations/de.json
  • app/_translations/en.json
  • app/_translations/es.json
  • app/_translations/fr.json
  • app/_translations/it.json
  • app/_translations/klingon.json
  • app/_translations/ko.json
  • app/_translations/nl.json
  • app/_translations/pirate.json
  • app/_translations/pl.json
  • app/_translations/pt.json
  • app/_translations/ru.json
  • app/_translations/tr.json
  • app/_translations/zh.json
  • app/_types/checklist.ts
  • app/_types/dnd.ts
  • app/_types/index.ts
  • app/_types/user.ts
  • app/_utils/checklist-utils.ts
  • app/_utils/client-parser-utils.ts
  • app/_utils/dnd/auto-scroll.ts
  • app/_utils/dnd/dnd-math.ts
  • app/_utils/dnd/drag-store.ts
  • app/_utils/dnd/rect-registry.ts
  • app/_utils/item-status-utils.ts
  • app/_utils/kanban/board-utils.ts
  • app/_utils/kanban/calendar-utils.ts
  • app/_utils/kanban/index.tsx
  • app/api/checklists/[listId]/items/[itemIndex]/route.ts
  • app/api/checklists/[listId]/items/reorder/route.ts
  • app/api/checklists/route.ts
  • app/api/docs/route.ts
  • app/api/search/route.ts
  • next.config.mjs
  • package.json
  • patches/body-size-limit_20260427.js
  • public/api/paths/checklists.yaml
  • public/api/paths/search.yaml
  • tests/server-actions/auth.test.ts
  • tests/server-actions/drop-item.test.ts
  • tests/utils/board-utils.test.ts
  • tests/utils/client-parser-utils.test.ts
  • tests/utils/connections-graph-data.test.ts
  • tests/utils/dnd-math.test.ts
  • tests/utils/item-status-utils.test.ts
💤 Files with no reviewable changes (1)
  • patches/body-size-limit_20260427.js

Comment thread app/api/checklists/[listId]/items/reorder/route.ts
Comment thread app/api/checklists/[listId]/items/reorder/route.ts
Comment thread package.json Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 16

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/_utils/checklist-utils.ts (1)

229-241: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

itemMetadata can overwrite the resolved id.

After setting fallback IDs (Lines 230 and 273), spreading ...itemMetadata (Lines 240 and 279) can restore invalid/duplicate id values. This parser also lacks a post-parse dedupe pass, so duplicates can survive into runtime state.

Suggested fix pattern
-          item = {
-            id: itemMetadata.id || generateItemId(parentLevel),
+          const { id: metadataId, ...restMetadata } = itemMetadata;
+          const resolvedId =
+            typeof metadataId === "string" && metadataId.trim()
+              ? metadataId
+              : generateItemId(parentLevel);
+          item = {
+            id: resolvedId,
             text: itemText,
             completed,
             order: currentItemIndex,
             status,
             timeEntries,
             estimatedTime,
             startDate,
             targetDate,
             description,
-            ...itemMetadata,
+            ...restMetadata,
             ...(recurrence ? { recurrence } : {}),
...
-          item = {
-            id: itemMetadata.id || generateItemId(parentLevel),
+          const { id: metadataId, ...restMetadata } = itemMetadata;
+          const resolvedId =
+            typeof metadataId === "string" && metadataId.trim()
+              ? metadataId
+              : generateItemId(parentLevel);
+          item = {
+            id: resolvedId,
             text: itemText,
             completed,
             order: currentItemIndex,
             description,
             ...metadata,
-            ...itemMetadata,
+            ...restMetadata,
             ...(recurrence && { recurrence }),
           };

Also applies to: 272-280

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/_utils/checklist-utils.ts` around lines 229 - 241, The constructed item
object currently spreads ...itemMetadata after assigning id (using
generateItemId(parentLevel)), which allows an invalid/duplicate id from
itemMetadata to overwrite the resolved id; change the construction so the
resolved id cannot be overwritten (either spread itemMetadata first and then set
id: itemMetadata.id || generateItemId(parentLevel), or remove/id-sanitize
itemMetadata.id before spreading), and add a post-parse dedupe pass in the
parser that scans generated items for duplicate ids and regenerates or reassigns
unique ids (use the same generateItemId logic) to ensure no duplicates survive
into runtime state; apply the same change to the other symmetric site where
items are built.
♻️ Duplicate comments (2)
app/api/checklists/[listId]/items/reorder/route.ts (2)

54-59: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Self-drop corrupts the item tree (still unresolved).

When activeItemId === overItemId, the code removes the item at line 86 then attempts to re-insert it relative to itself. In isDropInto mode, the item gets pushed into its own (now-orphaned) children; otherwise, findIndex returns -1 after removal, causing incorrect insertion.

Add an early return before the splice:

       if (!activeInfo || !overInfo) {
         return NextResponse.json({ error: "Item not found" }, { status: 404 });
       }
+
+      if (activeItemId === overItemId) {
+        return NextResponse.json({ success: true });
+      }

       activeInfo.siblings.splice(activeInfo.index, 1);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/api/checklists/`[listId]/items/reorder/route.ts around lines 54 - 59, Add
an early no-op return when the dragged item ID equals the target ID to avoid
removing and re-inserting the same node: check if activeItemId === overItemId
immediately after validating inputs (before the splice/removal logic that
currently mutates the tree) and return early (e.g., a 200/204 success or JSON
indicating no change) so that isDropInto and subsequent findIndex logic never
run on a now-orphaned node.

68-71: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Missing edit permission check on shared lists (still unresolved).

This route authenticates the caller via withApiAuth but does not verify PermissionTypes.EDIT before writing. On shared checklists, any authenticated user with API access can reorder items, bypassing the permission model enforced by other checklist mutation endpoints like status.ts and crud.ts.

       const list = await getListById(params.listId, user.username);
       if (!list) {
         return NextResponse.json({ error: "List not found" }, { status: 404 });
       }
+
+      const { checkUserPermission } = await import("`@/app/_server/actions/sharing`");
+      const canEdit = await checkUserPermission(
+        list.uuid || list.id,
+        list.category || "Uncategorized",
+        "checklist",
+        user.username,
+        "edit",
+      );
+      if (!canEdit) {
+        return NextResponse.json({ error: "Permission denied" }, { status: 403 });
+      }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/api/checklists/`[listId]/items/reorder/route.ts around lines 68 - 71,
After fetching the list with getListById in the reorder route, enforce the same
edit-permission check used by other checklist mutation endpoints: verify the
authenticated user has PermissionTypes.EDIT on that list (mirror the check logic
used in status.ts/crud.ts, e.g., call the project's helper/utility that checks
list permissions or inspect list.permissions for PermissionTypes.EDIT). If the
user lacks edit permission, return a 403 response (e.g., NextResponse.json({
error: "Forbidden" }, { status: 403 })) and do not perform the reorder; add this
check immediately after the existing list existence check and before any write
operations. Ensure you import or reference PermissionTypes and the same
permission-check helper used elsewhere so behavior is consistent.
🟡 Minor comments (15)
app/_translations/de.json-285-285 (1)

285-285: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Translate auth.rememberMe in the non-English locales.
This new login label is still hard-coded in English, so German and Spanish users will see an untranslated string.

  • app/_translations/de.json#L285-L285: replace "Remember me" with a German translation.
  • app/_translations/es.json#L287-L287: replace "Remember me" with a Spanish translation.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/_translations/de.json` at line 285, Replace the hard-coded English login
label for auth.rememberMe in both locale files: in app/_translations/de.json
(lines 285-285) change the value "Remember me" to the German translation
"Angemeldet bleiben"; in app/_translations/es.json (lines 287-287) change the
value "Remember me" to the Spanish translation "Recuérdame".
app/_translations/de.json-695-715 (1)

695-715: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Finish localizing the new profile graph labels.
Several newly added graph strings are still in English in both locales, and the German file also has a typo in validationErrorTitle (Validerungsfehler).

  • app/_translations/de.json#L695-L715: translate the remaining graph labels (visibleGraphSummary, graphTruncated, totalConnections, etc.) and fix the typo.
  • app/_translations/es.json#L695-L715: translate the remaining graph labels instead of shipping English fallbacks.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/_translations/de.json` around lines 695 - 715, In
app/_translations/de.json (lines 695-715) fix the typo by changing
validationErrorTitle value to "Validierungsfehler" and provide German
translations for the remaining keys visibleGraphSummary, graphTruncated,
totalConnections, inboundConnections, outboundConnections, updated, openItem,
selectItem, inspectNodeHint, graphSearchPlaceholder, orphans, labels, arrows,
nodeSize, linkWidth, linkDistance, repelForce, tagLinks, linkedItems, uuid; in
app/_translations/es.json (lines 695-715) replace the English fallbacks by
adding Spanish translations for the same keys (visibleGraphSummary,
graphTruncated, totalConnections, inboundConnections, outboundConnections,
updated, openItem, selectItem, inspectNodeHint, graphSearchPlaceholder, orphans,
labels, arrows, nodeSize, linkWidth, linkDistance, repelForce, tagLinks,
linkedItems, uuid) so both locale files contain localized strings and no English
remains.
app/_translations/fr.json-696-713 (1)

696-713: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

New locale keys were added without full localization, causing mixed-language UI.

  • app/_translations/fr.json#L696-L713: translate new profile graph labels/messages to French.
  • app/_translations/fr.json#L287-L287: translate auth.rememberMe to French.
  • app/_translations/it.json#L696-L713: translate new profile graph labels/messages to Italian.
  • app/_translations/it.json#L287-L287: translate auth.rememberMe to Italian.
  • app/_translations/klingon.json#L721-L739: align new profile graph labels/messages with the locale’s translation strategy.
  • app/_translations/klingon.json#L312-L312: localize auth.rememberMe consistently with the rest of the locale.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/_translations/fr.json` around lines 696 - 713, Update the
untranslated/new locale keys for each affected file: for
app/_translations/fr.json (lines 696-713) provide French translations for the
keys visibleGraphSummary, graphTruncated, totalConnections, inboundConnections,
outboundConnections, updated, openItem, selectItem, inspectNodeHint,
graphSearchPlaceholder, orphans, labels, arrows, nodeSize, linkWidth,
linkDistance, repelForce, tagLinks; for app/_translations/fr.json (line 287)
translate auth.rememberMe into French; for app/_translations/it.json (lines
696-713) provide Italian translations for the same profile graph keys; for
app/_translations/it.json (line 287) translate auth.rememberMe into Italian; for
app/_translations/klingon.json (lines 721-739) add/adjust the profile graph keys
to follow the locale’s existing translation style and conventions (use the same
key names as above); for app/_translations/klingon.json (line 312) localize
auth.rememberMe consistently with the rest of the Klingon locale. Ensure each
translation preserves the placeholder tokens ({items}, {links}, {visible},
{omitted}) exactly and keep key names unchanged.
app/_translations/ko.json-312-312 (1)

312-312: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Translate newly added Korean locale strings that are still in English.

Line 312 and Lines 721-738 are currently English in ko.json, which will produce mixed-language UI in login and the connections graph panel.

Also applies to: 721-738

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/_translations/ko.json` at line 312, Translate the remaining English
strings in the Korean locale file so the UI is consistent: replace the
"rememberMe" value with its Korean translation and locate and translate the
other English entries used by the login screen and the connections graph panel
(the remaining English strings in ko.json) to appropriate Korean text; ensure
you only change the values (not the keys) so keys like "rememberMe" remain
intact and update any punctuation/spacing to match existing translations.
app/_translations/pirate.json-312-312 (1)

312-312: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Keep new Pirate locale entries in the same voice as the rest of the file.

Line 312 and Lines 721-739 are plain English and break the established Pirate localization style for this locale.

Also applies to: 721-739

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/_translations/pirate.json` at line 312, The added translation
"rememberMe": "Remember me" is in plain English and breaks the Pirate locale
voice—replace its value with a Pirate-styled string (e.g., "Remind me, matey" or
similar) and likewise convert the other plain-English entries noted in the
review later in this file to match the established Pirate tone; update only the
translation values (leave keys like "rememberMe" unchanged) and ensure
punctuation/contractions match the existing Pirate phrasing used elsewhere in
the file.
app/_translations/nl.json-287-287 (1)

287-287: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Complete Dutch localization for new auth/graph keys.

Line 287 and Lines 696-713 are still English in nl.json; this causes mixed-language UX in authentication and graph controls.

Also applies to: 696-713

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/_translations/nl.json` at line 287, The "rememberMe" key currently has an
English value and several other auth/graph strings (lines 696–713) are still in
English; update the value of "rememberMe" to the Dutch translation "Onthoud mij"
and replace every remaining English string in that auth/graph block with their
correct Dutch equivalents so the nl.json contains fully Dutch text for the
authentication and graph control keys (ensure exact JSON keys are preserved,
only change the string values).
app/_translations/pt.json-711-729 (1)

711-729: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Translate the new graph-control labels in profile to Portuguese.

Lines 712-729 are still English (visibleGraphSummary, graphTruncated, totalConnections, openItem, graphSearchPlaceholder, etc.), causing mixed-language UI in the PT locale.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/_translations/pt.json` around lines 711 - 729, The PT translation file
still contains English strings for several graph-control keys; update the values
for the keys visibleGraphSummary, graphTruncated, totalConnections,
inboundConnections, outboundConnections, updated, openItem, selectItem,
inspectNodeHint, graphSearchPlaceholder, orphans, labels, arrows, nodeSize,
linkWidth, linkDistance, repelForce, and tagLinks in app/_translations/pt.json
to proper Portuguese equivalents (e.g., "visibleGraphSummary" -> "{items} itens
visíveis, {links} ligações visíveis", translate "graphTruncated" to something
like "Mostrando os {visible} itens com mais conexões. {omitted} itens com menos
conexões foram omitidos.", and localize the other keys similarly) so the PT
locale no longer mixes English and Portuguese.
app/_translations/ru.json-311-313 (1)

311-313: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Localize newly added Russian strings to avoid mixed-language UI.

Line 312 (auth.rememberMe) and Lines 721-738 (profile graph labels/controls) are still English. Russian users will see mixed-language text in login and connections graph screens.

Suggested patch (examples)
-    "rememberMe": "Remember me"
+    "rememberMe": "Запомнить меня"

-    "visibleGraphSummary": "{items} visible items, {links} visible links",
+    "visibleGraphSummary": "{items} видимых элементов, {links} видимых связей",
-    "totalConnections": "Total",
+    "totalConnections": "Всего",
-    "openItem": "Open item",
+    "openItem": "Открыть элемент"

Also applies to: 721-738

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/_translations/ru.json` around lines 311 - 313, The Russian locale
contains untranslated entries causing mixed-language UI: update the
"auth.rememberMe" key in app/_translations/ru.json to a proper Russian
translation and translate the profile graph labels/controls referenced as the
profile.* keys (the labels/controls shown around lines ~721-738) into Russian as
well; locate and replace the English strings with their Russian equivalents,
preserving the existing JSON keys and punctuation/escaping so only the values
change.
app/_translations/pl.json-286-287 (1)

286-287: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Localize newly added Polish strings to avoid mixed-language UI.

Line 287 (auth.rememberMe) and Lines 696-713 (profile graph labels) are still in English, while surrounding Polish copy is localized. This will surface mixed-language text in core UI flows.

Suggested patch (examples)
-    "rememberMe": "Remember me"
+    "rememberMe": "Zapamiętaj mnie"

-    "visibleGraphSummary": "{items} visible items, {links} visible links",
+    "visibleGraphSummary": "{items} widocznych elementów, {links} widocznych połączeń",
-    "totalConnections": "Total",
+    "totalConnections": "Łącznie",
-    "openItem": "Open item",
+    "openItem": "Otwórz element"

Also applies to: 696-713

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/_translations/pl.json` around lines 286 - 287, Update the untranslated
English strings in the translations JSON: replace the auth.rememberMe key value
with a Polish translation (e.g., "Zapamiętaj mnie") and translate the profile
graph label strings (the keys under "profile" that appear around lines 696–713)
into Polish so the UI is fully localized and consistent with surrounding
entries; keep the existing keys (auth.rememberMe and profile.*) unchanged and
only modify their string values.
app/_translations/pt.json-688-688 (1)

688-688: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix encoded quotes and punctuation in PT user-facing strings.

Line 688 and Line 1021 use literal &quot;, which will render incorrectly in plain text. Line 1022 is missing a closing parenthesis in the shortcut hint.

Suggested patch
-    "noArchivedItemsFound": "Nenhum item arquivado encontrado correspondente a &quot;{searchQuery}&quot;",
+    "noArchivedItemsFound": "Nenhum item arquivado encontrado correspondente a \"{searchQuery}\"",

-    "noEmojisYet": "Ainda não foram criados emojis personalizados. Clique em &quot;Adicionar emoji&quot; para começar.",
+    "noEmojisYet": "Ainda não foram criados emojis personalizados. Clique em \"Adicionar emoji\" para começar.",

-    "enterAnyEmoji": "Introduzir qualquer emoji ou usar o seletor de emoji (Windows: Win+.; Mac: Ctrl+Cmd+Space",
+    "enterAnyEmoji": "Introduzir qualquer emoji ou usar o seletor de emoji (Windows: Win+.; Mac: Ctrl+Cmd+Space)"

Also applies to: 1021-1022

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/_translations/pt.json` at line 688, The PT translation has HTML-encoded
double quotes and a missing parenthesis in a shortcut hint: replace the &quot;
sequences with plain double-quote characters in the "noArchivedItemsFound" value
and in any other PT strings containing &quot; (search the file for "&quot;"),
and add the missing closing parenthesis to the shortcut hint string referenced
in the PT file (fix the shortcut hint entry on the same section so it ends with
the closing ")").
app/_components/FeatureComponents/Kanban/KanbanCardDetail.tsx-310-326 (1)

310-326: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Optimistic update may leave UI inconsistent on server failure.

setStatusInput(status) is called before the server request. If updateItemStatus fails or returns success: false, the local statusInput state remains updated while the actual item status is unchanged.

Consider reverting the optimistic update on failure:

🛠️ Suggested fix
 const handleStatusChange = async (status: string) => {
+  const previousStatus = statusInput;
   setStatusInput(status);
   const formData = new FormData();
   formData.append("listId", checklistId);
   formData.append("itemId", item.id);
   formData.append("status", status);
   formData.append("category", category);
   const result = await updateItemStatus(formData);
   if (result.success && result.data) {
     onUpdate(result.data);
     const updatedItem = _findItemInChecklist(result.data, item.id);
     if (updatedItem) {
       setItem(updatedItem);
       setStatusInput(updatedItem.status || status);
     }
+  } else {
+    setStatusInput(previousStatus);
   }
 };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/_components/FeatureComponents/Kanban/KanbanCardDetail.tsx` around lines
310 - 326, The handler handleStatusChange is doing an optimistic update by
calling setStatusInput(status) before the network call; capture the previous
status (e.g., const prev = statusInput) then setStatusInput(status)
optimistically, await updateItemStatus(formData), and if the call fails or
returns result.success === false revert with setStatusInput(prev) (and avoid
calling onUpdate/setItem on failure); on success continue calling
onUpdate(result.data) and update item/state as now done. Also consider surfacing
an error (e.g., a toast) when reverting so users know the change did not
persist.
app/_hooks/useNoteEditor.tsx-170-180 (1)

170-180: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Missing dependency notesDefaultMode in effect.

The effect checks notesDefaultMode !== "edit" at line 171, but notesDefaultMode is not included in the dependency array. If notesDefaultMode changes (e.g., user preference update), the effect won't re-run.

🔧 Suggested fix
-  }, [contentIsDirty, title, category, note, isEditing]);
+  }, [contentIsDirty, title, category, note, isEditing, notesDefaultMode, setProviderUnsaved]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/_hooks/useNoteEditor.tsx` around lines 170 - 180, The useEffect in
useNoteEditor watches contentIsDirty, title, category, note, and isEditing but
omits notesDefaultMode even though the callback checks notesDefaultMode !==
"edit"; update the effect's dependency array to include notesDefaultMode so the
effect will re-run when the default mode changes, ensuring
setHasUnsavedChanges(...) and setProviderUnsaved(...) are recalculated
correctly; keep the existing logic in the effect (using title, category, note,
isEditing, contentIsDirty) and only modify the dependency list to add
notesDefaultMode.
app/_components/FeatureComponents/Kanban/KanbanColumn.tsx-144-152 (1)

144-152: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Close the archive-all confirmation modal after a successful archive.

The confirm handler never resets showArchiveAllModal, so the modal can remain open after a successful archive action.

Suggested fix
   const handleArchiveAll = async () => {
     if (!onArchiveAll || archivableCount === 0) return;
     setIsArchiving(true);
     try {
       await onArchiveAll();
+      setShowArchiveAllModal(false);
     } finally {
       setIsArchiving(false);
     }
   };

Also applies to: 244-247

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/_components/FeatureComponents/Kanban/KanbanColumn.tsx` around lines 144 -
152, The archive-all confirmation modal isn't being closed after a successful
archive; update the handler(s) to reset showArchiveAllModal only on success. In
the handleArchiveAll function (and the equivalent handler at the other site),
move or add setShowArchiveAllModal(false) into the try block immediately after
the awaited onArchiveAll() call so the modal closes on success, and keep
setIsArchiving(false) in the finally block to always clear the loading state.
app/_components/GlobalComponents/Auth/LoginForm.tsx-199-213 (1)

199-213: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Keep the remember-me text click target consistent with disabled state.

The text click handler still toggles state while the control is disabled (loading/SSO/lockout), which breaks expected disabled behavior.

Suggested fix
           <span
-            onClick={() => setRememberMe(!rememberMe)}
+            onClick={() => {
+              if (isLoading || isSsoLoading || countdown > 0) return;
+              setRememberMe((prev) => !prev);
+            }}
             className="text-sm lg:text-xs text-muted-foreground cursor-pointer select-none"
           >
             {t("rememberMe")}
           </span>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/_components/GlobalComponents/Auth/LoginForm.tsx` around lines 199 - 213,
The clickable label for the "remember me" toggle currently calls setRememberMe
even when the control is disabled (checked via isLoading, isSsoLoading,
countdown), so change the span's onClick to respect the same disabled condition
used by the Toggle: guard the handler so it only calls
setRememberMe(!rememberMe) when isLoading === false && isSsoLoading === false &&
countdown === 0 (or remove the handler when those conditions are true); also
update the span's className to remove cursor-pointer / use a non-interactive
style when disabled so visual affordance matches behavior. Reference: the Toggle
component/props, rememberMe state, setRememberMe, isLoading, isSsoLoading,
countdown and the span element in LoginForm.tsx.
app/_server/actions/users/crud.ts-207-207 (1)

207-207: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Keep user default preference initialization consistent across creation flows.

Line 207 sets hideMobileStatusDropdown: "disable" in createUser, but register in app/_server/actions/auth/index.ts still creates users without this field. This makes defaults depend on how the account was created.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/_server/actions/users/crud.ts` at line 207, The createUser flow sets the
user preference hideMobileStatusDropdown: "disable" but the register flow in
register() when creating a new user omits this field, causing inconsistent
defaults; update the register user-creation logic to include
hideMobileStatusDropdown: "disable" (or better, refactor both createUser and
register to use a shared defaultPreferences object) so newly registered users
get the same hideMobileStatusDropdown default as users created via createUser.
🧹 Nitpick comments (7)
app/_components/FeatureComponents/Kanban/KanbanCard.tsx (1)

98-102: 💤 Low value

Missing dependency in useEffect.

The useEffect references kanbanItemHook.stopTimerOnDrag but doesn't include it in the dependency array. While this likely works because stopTimerOnDrag is stable, ESLint's exhaustive-deps rule will warn. Consider adding the dependency or using a ref to silence the warning explicitly.

   useEffect(() => {
     if (isLifted) {
       kanbanItemHook.stopTimerOnDrag();
     }
-  }, [isLifted]);
+  }, [isLifted, kanbanItemHook.stopTimerOnDrag]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/_components/FeatureComponents/Kanban/KanbanCard.tsx` around lines 98 -
102, The useEffect in KanbanCard.tsx that calls kanbanItemHook.stopTimerOnDrag
when isLifted changes is missing kanbanItemHook.stopTimerOnDrag in its
dependency array; update the effect to include kanbanItemHook.stopTimerOnDrag
(or, if you prefer to keep a stable reference, create a ref or memoized callback
in the hook and reference that) so the dependency array lists both isLifted and
kanbanItemHook.stopTimerOnDrag and ESLint exhaustive-deps warnings are resolved.
app/_components/FeatureComponents/Kanban/Kanban.tsx (1)

154-208: ⚖️ Poor tradeoff

Sequential archiving may be slow for large columns.

handleArchiveAll awaits each archiveItem call sequentially. For columns with many items, this could take significant time and leave the UI in an intermediate state.

Consider batching or parallel execution with Promise.all (if the server supports it), or at minimum show a loading indicator during the operation.

app/_utils/kanban/calendar-utils.ts (1)

39-40: 💤 Low value

Minor: Redundant normalization in return object.

Both startDate and endDate assignments perform the same comparison. Since startDate is already normalized to be <= endDate on line 39, line 40's comparison is redundant.

       return {
         id: item.id,
         title: item.text,
         startDate: startDate <= endDate ? startDate : endDate,
-        endDate: endDate >= startDate ? endDate : startDate,
+        endDate: startDate <= endDate ? endDate : startDate,

This is functionally equivalent but slightly clearer that one condition determines both assignments. However, current code is still correct.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/_utils/kanban/calendar-utils.ts` around lines 39 - 40, The return object
does redundant comparisons for startDate/endDate; compute the boolean once
(e.g., const isStartBefore = startDate <= endDate) and use it to assign both
fields so startDate = isStartBefore ? startDate : endDate and endDate =
isStartBefore ? endDate : startDate, replacing the two repeated ternaries for
clarity.
app/_components/FeatureComponents/Notes/Parts/TipTap/CustomExtensions/SlashCommands.tsx (1)

351-359: 💤 Low value

Unbounded results may cause performance or usability issues with large datasets.

Removing the result limit means all matching items are returned. For users with hundreds of notes/checklists, this could cause:

  1. Slow rendering of the suggestion dropdown
  2. Overwhelming UI with too many options

Consider adding a reasonable upper limit while keeping the full list for empty queries:

💡 Optional: Add reasonable limit
-          if (!query.trim()) return allItems;
+          if (!query.trim()) return allItems.slice(0, 50);

           const lowerCaseQuery = query.toLowerCase();

-          return allItems.filter(
+          return allItems.filter(
             (item) =>
               item.title.toLowerCase().includes(lowerCaseQuery) ||
               item.category?.toLowerCase().includes(lowerCaseQuery)
-          );
+          ).slice(0, 50);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@app/_components/FeatureComponents/Notes/Parts/TipTap/CustomExtensions/SlashCommands.tsx`
around lines 351 - 359, The current filtering returns all matching items (using
allItems, query and lowerCaseQuery) which can be unbounded; add a reasonable cap
(e.g. const MAX_SUGGESTIONS = 50) and apply it after filtering so that the
returned array is limited (use .filter(...).slice(0, MAX_SUGGESTIONS)), leaving
the early-return for empty queries if you want the full list shown when the
query is blank; update any related types/consumers if they assume unlimited
results.
app/api/search/route.ts (1)

3-3: ⚡ Quick win

Unused import search.

The search function is imported but never used in this file.

-import { search } from "`@/app/_server/actions/search`";
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/api/search/route.ts` at line 3, The import of the symbol "search" is
unused; either remove the unused import line "import { search } from
'`@/app/_server/actions/search`';" from route.ts or, if the endpoint should call
that function, replace the unused import by invoking search inside the exported
handler (e.g., call search(...) and return its result) and remove any other
redundant code accordingly; ensure there are no leftover linter warnings after
the change.
tests/server-actions/auth.test.ts (1)

346-351: ⚡ Quick win

expect.any(Boolean) is too loose for remember-me regression coverage.

This assertion won’t catch incorrect mapping of remember-me state. Add explicit cases (e.g., rememberMe=true and rememberMe=false) and assert exact fourth-argument values.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/server-actions/auth.test.ts` around lines 346 - 351, The assertion
using expect.any(Boolean) is too loose for remember-me regression coverage;
update the test(s) that call mockCreateSession so the fourth argument is
asserted explicitly for both rememberMe=true and rememberMe=false (either by
adding two test cases or parameterizing the test) — replace expect.any(Boolean)
with the exact boolean values (true for the remember-me case and false for the
non-remember case) when calling
expect(mockCreateSession).toHaveBeenCalledWith(...), keeping the other arguments
as expect.any(String), 'alice', 'ldap' as before.
tests/utils/connections-graph-data.test.ts (1)

244-278: Replace source-string assertions with behavior-based tests for LinksTab/ConnectionsGraph.

In tests/utils/connections-graph-data.test.ts (lines 244-278), these tests read component TSX via readFileSync and assert exact substrings (order-* class order, t("profile.unknown"), and internal constants like MIN_POINTER_RADIUS / nodePointerAreaPaint / enableNodeDrag). Render LinksTab/ConnectionsGraph and assert observable behavior instead (mobile vs desktop DOM order, rendered label text, and pointer/drag behavior under coarse vs fine pointer).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/utils/connections-graph-data.test.ts` around lines 244 - 278, The tests
currently assert literal source substrings; instead render the actual components
and assert behavior: replace the file-string checks in the test for LinksTab by
rendering the LinksTab component and asserting DOM order (querySelectorAll for
the graph and controls and verify graph appears before controls when simulating
mobile and after when simulating desktop by toggling window.innerWidth and
dispatching a resize or mocking matchMedia), replace the translation key check
by rendering LinksTab with your i18n/test translation provider (or a mocked t)
and asserting the rendered label text uses the profile.unknown translation, and
replace the ConnectionsGraph string checks by rendering ConnectionsGraph and
asserting pointer/drag behavior: mock matchMedia('(pointer: coarse)') to
simulate coarse vs fine pointers and assert enableNodeDrag effect (e.g.,
presence/absence of drag handlers or draggable attributes) and verify the
pointer hit area size is applied in the DOM (computed radius or attribute
reflecting MIN_POINTER_RADIUS / globalScale) rather than searching for
MIN_POINTER_RADIUS/nodePointerAreaPaint/globalScale strings.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: ee6ec96d-ea2c-4a83-8bfd-d6cf21662cb5

📥 Commits

Reviewing files that changed from the base of the PR and between 3a7c22f and 999a421.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (91)
  • app/(loggedInRoutes)/settings/connections/page.tsx
  • app/_components/FeatureComponents/Home/Parts/ChecklistHome.tsx
  • app/_components/FeatureComponents/Home/Parts/NotesHome.tsx
  • app/_components/FeatureComponents/Kanban/ArchivedItemsModal.tsx
  • app/_components/FeatureComponents/Kanban/CalendarView.tsx
  • app/_components/FeatureComponents/Kanban/Kanban.tsx
  • app/_components/FeatureComponents/Kanban/KanbanCard.tsx
  • app/_components/FeatureComponents/Kanban/KanbanCardDetail.tsx
  • app/_components/FeatureComponents/Kanban/KanbanCardDetailProperties.tsx
  • app/_components/FeatureComponents/Kanban/KanbanColumn.tsx
  • app/_components/FeatureComponents/Kanban/KanbanItemContent.tsx
  • app/_components/FeatureComponents/Notes/Parts/MermaidRenderer.tsx
  • app/_components/FeatureComponents/Notes/Parts/TipTap/CustomExtensions/ExcalidrawExtension.tsx
  • app/_components/FeatureComponents/Notes/Parts/TipTap/CustomExtensions/MermaidExtension.tsx
  • app/_components/FeatureComponents/Notes/Parts/TipTap/CustomExtensions/SlashCommands.tsx
  • app/_components/FeatureComponents/Profile/Parts/ConnectionsGraph/ConnectionsGraph.tsx
  • app/_components/FeatureComponents/Profile/Parts/ConnectionsGraph/graph-data.ts
  • app/_components/FeatureComponents/Profile/Parts/LinksTab.tsx
  • app/_components/FeatureComponents/Profile/Parts/UserPreferencesTab.tsx
  • app/_components/GlobalComponents/Auth/LoginForm.tsx
  • app/_components/GlobalComponents/Dropdowns/Dropdown.tsx
  • app/_components/GlobalComponents/Modals/Modal.tsx
  • app/_components/GlobalComponents/Sidebar/SidebarWrapper.tsx
  • app/_consts/dnd.ts
  • app/_hooks/dnd/DndProvider.tsx
  • app/_hooks/dnd/index.tsx
  • app/_hooks/dnd/useDragItem.ts
  • app/_hooks/dnd/useDropList.ts
  • app/_hooks/kanban/useCalendarView.ts
  • app/_hooks/kanban/useKanban.ts
  • app/_hooks/kanban/useKanbanDnd.ts
  • app/_hooks/kanban/useKanbanItem.tsx
  • app/_hooks/useChecklist.tsx
  • app/_hooks/useNoteEditor.tsx
  • app/_providers/NavigationGuardProvider.tsx
  • app/_schemas/user-schemas.ts
  • app/_server/actions/auth/index.ts
  • app/_server/actions/checklist-item/crud.ts
  • app/_server/actions/checklist-item/drop.ts
  • app/_server/actions/checklist-item/index.ts
  • app/_server/actions/checklist-item/status.ts
  • app/_server/actions/file/index.ts
  • app/_server/actions/note/parsers.ts
  • app/_server/actions/session/index.ts
  • app/_server/actions/users/crud.ts
  • app/_styles/globals.css
  • app/_translations/de.json
  • app/_translations/en.json
  • app/_translations/es.json
  • app/_translations/fr.json
  • app/_translations/it.json
  • app/_translations/klingon.json
  • app/_translations/ko.json
  • app/_translations/nl.json
  • app/_translations/pirate.json
  • app/_translations/pl.json
  • app/_translations/pt.json
  • app/_translations/ru.json
  • app/_translations/tr.json
  • app/_translations/zh.json
  • app/_types/checklist.ts
  • app/_types/dnd.ts
  • app/_types/index.ts
  • app/_types/user.ts
  • app/_utils/checklist-utils.ts
  • app/_utils/client-parser-utils.ts
  • app/_utils/dnd/auto-scroll.ts
  • app/_utils/dnd/dnd-math.ts
  • app/_utils/dnd/drag-store.ts
  • app/_utils/dnd/rect-registry.ts
  • app/_utils/item-status-utils.ts
  • app/_utils/kanban/board-utils.ts
  • app/_utils/kanban/calendar-utils.ts
  • app/_utils/kanban/index.tsx
  • app/api/checklists/[listId]/items/[itemIndex]/route.ts
  • app/api/checklists/[listId]/items/reorder/route.ts
  • app/api/checklists/route.ts
  • app/api/docs/route.ts
  • app/api/search/route.ts
  • next.config.mjs
  • package.json
  • patches/body-size-limit_20260427.js
  • public/api/paths/checklists.yaml
  • public/api/paths/search.yaml
  • tests/server-actions/auth.test.ts
  • tests/server-actions/drop-item.test.ts
  • tests/utils/board-utils.test.ts
  • tests/utils/client-parser-utils.test.ts
  • tests/utils/connections-graph-data.test.ts
  • tests/utils/dnd-math.test.ts
  • tests/utils/item-status-utils.test.ts
💤 Files with no reviewable changes (1)
  • patches/body-size-limit_20260427.js

Comment thread app/_hooks/dnd/DndProvider.tsx Outdated
Comment thread app/_hooks/kanban/useKanban.ts
Comment thread app/_hooks/kanban/useKanbanDnd.ts
Comment thread app/_hooks/kanban/useKanbanDnd.ts Outdated
Comment thread app/_server/actions/checklist-item/drop.ts Outdated
Comment thread app/_translations/zh.json Outdated
Comment thread app/_utils/client-parser-utils.ts
Comment thread app/_utils/dnd/rect-registry.ts
Comment thread app/api/search/route.ts
Comment thread next.config.mjs
Repository owner deleted a comment from coderabbitai Bot Jun 13, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/_utils/kanban/board-utils.ts (1)

13-31: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Keep the first-column fallback consistent with getColumnItems().

getColumnItems() maps unset or unknown statuses into the lowest-ordered status, but applyDrop() still compares against TaskStatus.TODO. On boards whose first status is not TODO, dragging an unset legacy card within that first column will incorrectly persist a status change and can trigger completeParent() side effects.

Proposed fix
 const _renumber = (items: Item[]): Item[] =>
   items.map((item, idx) => ({
     ...item,
     order: idx,
     children: item.children ? _renumber(item.children) : item.children,
   }));
+
+const _effectiveStatus = (
+  status: string | undefined,
+  statuses: KanbanStatus[] | undefined,
+): string => {
+  const statusList = statuses || DEFAULT_KANBAN_STATUSES;
+  const firstStatus =
+    [...statusList].sort((a, b) => a.order - b.order)[0]?.id || TaskStatus.TODO;
+  const validIds = new Set(statusList.map((s) => s.id));
+  return validIds.has(status || "") ? (status as string) : firstStatus;
+};
 
 export const getColumnItems = (
   items: Item[],
   statusId: string,
   statuses: KanbanStatus[] | undefined,
@@
-  const isStatusChange = (dragged.status || TaskStatus.TODO) !== targetStatus;
+  const isStatusChange =
+    _effectiveStatus(dragged.status, checklist.statuses) !== targetStatus;

Also applies to: 64-80

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/_utils/kanban/board-utils.ts` around lines 13 - 31, getColumnItems()
treats unknown/unset statuses as the board's lowest-ordered status, but
applyDrop() still uses TaskStatus.TODO as the first-column fallback; update
applyDrop() to compute the same firstStatus logic as getColumnItems (derive
statusList = statuses || DEFAULT_KANBAN_STATUSES, sort by order and take
firstStatus = [...statusList].sort((a,b)=>a.order-b.order)[0]?.id ||
TaskStatus.TODO) and use that firstStatus wherever applyDrop() currently
compares against TaskStatus.TODO or decides to map unknown/empty item.status to
the first column, so dragging legacy/unset cards in a non-TODO first column will
persist the correct status and avoid spurious completeParent() side effects.
♻️ Duplicate comments (1)
app/_server/actions/checklist-item/status.ts (1)

51-63: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Validate each timeEntries element before persisting it.

Lines 51-63 only reject non-arrays. Payloads like [null] or ["x"] still pass, then Lines 91-93 either throw on entry.user or serialize garbage into the markdown file. Please reject any element that is not a non-null object before calling updateItem.

Proposed fix
     if (timeEntriesStr) {
       try {
         const parsed = JSON.parse(timeEntriesStr);
-        if (!Array.isArray(parsed)) {
-          throw new Error("timeEntries must be an array");
+        if (
+          !Array.isArray(parsed) ||
+          parsed.some(
+            (entry) =>
+              !entry || typeof entry !== "object" || Array.isArray(entry),
+          )
+        ) {
+          throw new Error("timeEntries must be an array of objects");
         }
         parsedTimeEntries = parsed;
       } catch (e) {

Also applies to: 88-94

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/_server/actions/checklist-item/status.ts` around lines 51 - 63, The JSON
parsing block that sets parsedTimeEntries must validate that every element is a
non-null object before accepting the array: after JSON.parse and Array.isArray
check, iterate the array and ensure each element is typeof "object" and !== null
(and optionally has expected keys like user/start/end if you want stricter
checks); if any element fails, log and return the same { success: false, error:
"Invalid timeEntries payload" } rather than assigning parsedTimeEntries. Also
add the same defensive validation immediately before calling updateItem (the
place that reads entry.user) so malformed payloads are rejected even if upstream
parsing changed; reference parsedTimeEntries and the updateItem call to locate
where to add these checks.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/_components/FeatureComponents/Kanban/CalendarView.tsx`:
- Around line 157-168: The code currently renders every segment as a
keyboard-focusable button using role="button", tabIndex, onClick and onKeyDown
and always calls _openItem, which creates dead tab stops when onItemClick is not
provided; update the segment rendering so the button semantics and handlers are
only applied when onItemClick exists (i.e., conditionally add role="button",
tabIndex, onClick={() => _openItem(...)} and the onKeyDown handler, and keep the
non-interactive markup—a span/div with the same styling but without
cursor-pointer or focusable attributes—when onItemClick is undefined), ensuring
you reference the existing _openItem and onItemClick symbols and preserve visual
styles for the non-interactive case.

---

Outside diff comments:
In `@app/_utils/kanban/board-utils.ts`:
- Around line 13-31: getColumnItems() treats unknown/unset statuses as the
board's lowest-ordered status, but applyDrop() still uses TaskStatus.TODO as the
first-column fallback; update applyDrop() to compute the same firstStatus logic
as getColumnItems (derive statusList = statuses || DEFAULT_KANBAN_STATUSES, sort
by order and take firstStatus =
[...statusList].sort((a,b)=>a.order-b.order)[0]?.id || TaskStatus.TODO) and use
that firstStatus wherever applyDrop() currently compares against TaskStatus.TODO
or decides to map unknown/empty item.status to the first column, so dragging
legacy/unset cards in a non-TODO first column will persist the correct status
and avoid spurious completeParent() side effects.

---

Duplicate comments:
In `@app/_server/actions/checklist-item/status.ts`:
- Around line 51-63: The JSON parsing block that sets parsedTimeEntries must
validate that every element is a non-null object before accepting the array:
after JSON.parse and Array.isArray check, iterate the array and ensure each
element is typeof "object" and !== null (and optionally has expected keys like
user/start/end if you want stricter checks); if any element fails, log and
return the same { success: false, error: "Invalid timeEntries payload" } rather
than assigning parsedTimeEntries. Also add the same defensive validation
immediately before calling updateItem (the place that reads entry.user) so
malformed payloads are rejected even if upstream parsing changed; reference
parsedTimeEntries and the updateItem call to locate where to add these checks.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 9b31e4e4-077a-4658-b9b5-3b741e192eb5

📥 Commits

Reviewing files that changed from the base of the PR and between 999a421 and 59823e1.

📒 Files selected for processing (29)
  • app/_components/FeatureComponents/Kanban/CalendarView.tsx
  • app/_components/FeatureComponents/Kanban/KanbanCardDetail.tsx
  • app/_components/FeatureComponents/Notes/Parts/TipTap/CustomExtensions/ExcalidrawExtension.tsx
  • app/_components/FeatureComponents/Profile/Parts/ConnectionsGraph/graph-data.ts
  • app/_components/GlobalComponents/Auth/LoginForm.tsx
  • app/_hooks/kanban/useKanbanDnd.ts
  • app/_providers/NavigationGuardProvider.tsx
  • app/_server/actions/checklist-item/drop.ts
  • app/_server/actions/checklist-item/status.ts
  • app/_server/actions/file/index.ts
  • app/_translations/de.json
  • app/_translations/es.json
  • app/_translations/fr.json
  • app/_translations/it.json
  • app/_translations/klingon.json
  • app/_translations/ko.json
  • app/_translations/nl.json
  • app/_translations/pirate.json
  • app/_translations/pl.json
  • app/_translations/pt.json
  • app/_translations/ru.json
  • app/_translations/tr.json
  • app/_translations/zh.json
  • app/_utils/dnd/rect-registry.ts
  • app/_utils/item-status-utils.ts
  • app/_utils/kanban/board-utils.ts
  • app/api/checklists/[listId]/items/[itemIndex]/route.ts
  • app/api/checklists/[listId]/items/reorder/route.ts
  • public/api/paths/checklists.yaml
✅ Files skipped from review due to trivial changes (9)
  • app/_translations/tr.json
  • app/_translations/pt.json
  • app/_translations/pirate.json
  • app/_translations/pl.json
  • app/_translations/ko.json
  • app/_translations/de.json
  • app/_translations/es.json
  • app/_translations/it.json
  • app/_translations/fr.json
🚧 Files skipped from review as they are similar to previous changes (15)
  • app/_providers/NavigationGuardProvider.tsx
  • app/_server/actions/file/index.ts
  • app/_components/FeatureComponents/Notes/Parts/TipTap/CustomExtensions/ExcalidrawExtension.tsx
  • app/_server/actions/checklist-item/drop.ts
  • app/_utils/item-status-utils.ts
  • app/_components/GlobalComponents/Auth/LoginForm.tsx
  • app/_translations/zh.json
  • app/_components/FeatureComponents/Kanban/KanbanCardDetail.tsx
  • app/api/checklists/[listId]/items/[itemIndex]/route.ts
  • app/_components/FeatureComponents/Profile/Parts/ConnectionsGraph/graph-data.ts
  • app/_translations/klingon.json
  • app/_translations/ru.json
  • app/_translations/nl.json
  • app/_hooks/kanban/useKanbanDnd.ts
  • public/api/paths/checklists.yaml

Comment thread app/_components/FeatureComponents/Kanban/CalendarView.tsx Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
package.json (1)

121-121: Re-check esbuild 0.28.1 impact from resolutions

package.json pins "resolutions": { "esbuild": "0.28.1" }, and yarn.lock resolves esbuild to 0.28.1 only. esbuild 0.28.0 includes a breaking change: integrity checks for its fallback binary download path—so validate that installs/builds work in any environment where the fallback download path could be exercised.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@package.json` at line 121, package.json currently forces esbuild via
"resolutions": { "esbuild": "0.28.1" } which follows a breaking change
introduced in 0.28.0 around integrity checks for fallback binary downloads;
reproduce and validate installs and builds in environments that may hit the
fallback path (offline/behind mirrors, CI runners, and development machines with
restricted network) and confirm esbuild binary acquisition succeeds; if failures
occur, either (A) change the resolution to a known-compatible upstream version
(e.g., bump to a patched release that restores expected behavior) or (B) remove
the forced resolution and allow the package manager to select a compatible
esbuild, and add CI jobs that run npm/yarn install + build in representative
restricted-network scenarios to prevent regressions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@package.json`:
- Line 121: package.json currently forces esbuild via "resolutions": {
"esbuild": "0.28.1" } which follows a breaking change introduced in 0.28.0
around integrity checks for fallback binary downloads; reproduce and validate
installs and builds in environments that may hit the fallback path
(offline/behind mirrors, CI runners, and development machines with restricted
network) and confirm esbuild binary acquisition succeeds; if failures occur,
either (A) change the resolution to a known-compatible upstream version (e.g.,
bump to a patched release that restores expected behavior) or (B) remove the
forced resolution and allow the package manager to select a compatible esbuild,
and add CI jobs that run npm/yarn install + build in representative
restricted-network scenarios to prevent regressions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 78484d16-a5a9-47fd-b41c-f00904283b8a

📥 Commits

Reviewing files that changed from the base of the PR and between 59823e1 and 2d05c95.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (1)
  • package.json

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (2)
tests/api/tasks.test.ts (1)

608-679: ⚡ Quick win

Add missing negative-path tests for the new task-item GET handler.

The new suite covers happy paths and non-task type, but not unauthorized or malformed/out-of-range itemIndex branches. Adding those assertions would lock down route behavior.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/api/tasks.test.ts` around lines 608 - 679, The tests currently cover
happy paths and non-task types but miss negative branches for authorization and
invalid/out-of-range itemIndex; add tests that call GET_TASK_ITEM using
createMockRequest and the same params setup but (1) simulate missing/invalid
auth (omit auth header or mock auth failure) with mockGetListById resolved to
mockTask and assert response.status is 401 and error indicates unauthorized, and
(2) add malformed itemIndex cases (e.g., "abc") and out-of-range indices (e.g.,
"999" or "0.999") with mockGetListById resolved to mockTask and assert
response.status is 400 (or the handler's expected error code) and the error
message matches the handler's validation output; keep using getResponseJson to
inspect body and reuse existing helpers/mocks (mockGetListById,
createMockRequest, GET_TASK_ITEM).
public/api/paths/tasks.yaml (1)

564-576: ⚡ Quick win

Document the auth failure response for the new task-item GET operation.

This endpoint is authenticated, but the new operation currently omits a 401 response in the spec. Adding it keeps client/server contracts aligned.

Suggested OpenAPI addition
       '400':
         description: Invalid item index or not a task checklist
+      '401':
+        description: Unauthorized
       '404':
         description: Task or item not found
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@public/api/paths/tasks.yaml` around lines 564 - 576, The new GET task-item
operation is missing a 401 response; add a '401' response entry to the
operation's responses (next to '200', '400', '404') with description
"Unauthorized" and include an application/json content block that uses your
existing error response schema (e.g. reference '`#/components/schemas/Error`' or
'ErrorResponse'); if no shared error schema exists, return a small object schema
with a message string to represent the auth failure.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/api/checklists/`[listId]/items/[itemIndex]/route.ts:
- Line 162: The code currently skips appending text when it's an empty string
because it checks `if (text)`; change this to append when `text` is present in
the payload (i.e., not undefined/null) so empty-string updates are sent. Replace
the falsy check around `formData.append("text", text)` with an explicit
existence check such as `if (text !== undefined && text !== null)` (or `if
(typeof text !== "undefined")`) so that `text: ""` is included; this aligns with
the `hasField` logic that treats an empty string as an intended update.

In `@howto/API.md`:
- Around line 396-397: The examples use "todo" but the documented canonical
status set lists "in_progress", "paused", and "completed"; update the examples
so the "status" field uses one of the documented values (e.g., change "status":
"todo" to "status": "in_progress" or "status": "completed"), or alternatively
update the canonical status list to include "todo" if that is intended; ensure
all examples (including the other occurrence noted) consistently match the
chosen canonical set.

---

Nitpick comments:
In `@public/api/paths/tasks.yaml`:
- Around line 564-576: The new GET task-item operation is missing a 401
response; add a '401' response entry to the operation's responses (next to
'200', '400', '404') with description "Unauthorized" and include an
application/json content block that uses your existing error response schema
(e.g. reference '`#/components/schemas/Error`' or 'ErrorResponse'); if no shared
error schema exists, return a small object schema with a message string to
represent the auth failure.

In `@tests/api/tasks.test.ts`:
- Around line 608-679: The tests currently cover happy paths and non-task types
but miss negative branches for authorization and invalid/out-of-range itemIndex;
add tests that call GET_TASK_ITEM using createMockRequest and the same params
setup but (1) simulate missing/invalid auth (omit auth header or mock auth
failure) with mockGetListById resolved to mockTask and assert response.status is
401 and error indicates unauthorized, and (2) add malformed itemIndex cases
(e.g., "abc") and out-of-range indices (e.g., "999" or "0.999") with
mockGetListById resolved to mockTask and assert response.status is 400 (or the
handler's expected error code) and the error message matches the handler's
validation output; keep using getResponseJson to inspect body and reuse existing
helpers/mocks (mockGetListById, createMockRequest, GET_TASK_ITEM).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 855ff8b3-26d3-4da9-bcbc-5e38bf79e8ea

📥 Commits

Reviewing files that changed from the base of the PR and between ad55b04 and e732198.

📒 Files selected for processing (12)
  • app/_utils/api-item.ts
  • app/api/checklists/[listId]/items/[itemIndex]/route.ts
  • app/api/checklists/route.ts
  • app/api/tasks/[taskId]/items/[itemIndex]/route.ts
  • app/api/tasks/[taskId]/route.ts
  • app/api/tasks/route.ts
  • howto/API.md
  • public/api/components/schemas.yaml
  • public/api/paths/checklists.yaml
  • public/api/paths/tasks.yaml
  • tests/api/items.test.ts
  • tests/api/tasks.test.ts

Comment thread app/api/checklists/[listId]/items/[itemIndex]/route.ts Outdated
Comment thread howto/API.md Outdated
@fccview fccview merged commit fecc3ec into main Jun 14, 2026
6 checks passed
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.

[FEATURE]: onbeforeunload "Changes that you made may not be saved"

4 participants