Skip to content

PY-64: Notes#64

Merged
kaesetoast merged 6 commits into
mainfrom
py-64-notes
Jun 24, 2026
Merged

PY-64: Notes#64
kaesetoast merged 6 commits into
mainfrom
py-64-notes

Conversation

@kaesetoast

@kaesetoast kaesetoast commented Jun 24, 2026

Copy link
Copy Markdown
Member

Task

PY-64: Notes

Summary

Summary by CodeRabbit

  • New Features

    • Added a notes workspace with note creation, editing, archiving, labeling, linking, attachments, and a dedicated notes list/detail experience.
    • Introduced note labels with color editing, filtering, and management screens.
    • Added faster navigation and keyboard shortcut support for creating a new note.
  • Bug Fixes

    • Improved note and attachment handling so items stay in sync across lists and detail views.
    • Made date/time display consistent across browsers and locales.

@netlify

netlify Bot commented Jun 24, 2026

Copy link
Copy Markdown

Deploy Preview for pno-project-y ready!

Name Link
🔨 Latest commit dfb340f
🔍 Latest deploy log https://app.netlify.com/projects/pno-project-y/deploys/6a3bbddc9b46b50008310902
😎 Deploy Preview https://deploy-preview-64--pno-project-y.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@coderabbitai

coderabbitai Bot commented Jun 24, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@kaesetoast, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 35 minutes and 28 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 86dcee7b-d139-46cf-bc0d-35b05034e138

📥 Commits

Reviewing files that changed from the base of the PR and between 92c7c8a and dfb340f.

📒 Files selected for processing (30)
  • drizzle/0010_note_labels_note_idx.sql
  • drizzle/meta/_journal.json
  • src/components/AttachmentArea.tsx
  • src/components/AttachmentListItem.tsx
  • src/components/PageLayout.tsx
  • src/components/notes/CreateNoteButton.tsx
  • src/components/notes/NoteEditor.tsx
  • src/components/notes/NoteLabelSelect.tsx
  • src/components/notes/NoteListFilters.tsx
  • src/components/notes/NotePageContent.tsx
  • src/components/notes/use-note-label-color.ts
  • src/components/site-header.tsx
  • src/components/taxonomy/TaxonomyColorPicker.tsx
  • src/components/taxonomy/TaxonomyListItem.tsx
  • src/components/taxonomy/TaxonomyManagementSection.tsx
  • src/components/taxonomy/TaxonomySortableList.tsx
  • src/db/mutations/attachments.ts
  • src/db/mutations/entity-links.ts
  • src/db/mutations/note-labels.ts
  • src/db/mutations/notes.ts
  • src/db/queries/linkable-resources.ts
  • src/db/schema.ts
  • src/lib/format-relative-time.ts
  • src/lib/notes/filter-notes.test.ts
  • src/lib/notes/filter-notes.ts
  • src/lib/notes/note-activity.test.ts
  • src/lib/notes/note-activity.ts
  • src/routes/_signed-in/note-labels.tsx
  • src/routes/_signed-in/notes.$noteId.tsx
  • src/routes/_signed-in/notes.index.tsx
📝 Walkthrough

Walkthrough

This PR introduces a full notes feature: three DB migrations add notes, user-scoped note labels, entity links, and archive support; server mutations and React Query hooks handle CRUD and live sync; a TipTap editor, split-layout detail page, sidebar (attachments/links/activity), list with filtering/grouping, and label management UI are all wired to new routes. The existing Labels and Statuses taxonomy pages are refactored to use a new reusable TaxonomyManagementSection.

Changes

Notes System

Layer / File(s) Summary
DB schema, enums, and migrations
drizzle/0007_chemical_speedball.sql, drizzle/0008_note_labels.sql, drizzle/0009_note_archive.sql, drizzle/meta/_journal.json, src/db/schema.ts
Adds note_visibility, entity_link_type, entity_link_kind enums; creates note, note_label, note_labels_to_notes, entity_link tables; extends attachments with optional noteId; adds archive column; migrates shared labels to user-scoped labels; registers three new migration journal entries.
Note utility functions and tests
src/lib/notes/derive-title.ts, src/lib/notes/filter-notes.ts, src/lib/notes/list-preview.ts, src/lib/notes/note-activity.ts, src/lib/notes/entity-link-utils.ts, src/lib/notes/linkable-resources.ts, src/lib/format-relative-time.ts, src/lib/page-layout-width.ts, src/lib/notes/*.test.ts, src/lib/format-relative-time.test.ts
Adds pure helpers for title derivation, note list filtering/grouping, activity building, entity link traversal, and linkable resource filtering; pins locale to en-US for time formatting; adds Vitest suites for all new utilities.
Server mutations and queries for notes, note labels, entity links, and attachments
src/db/mutations/notes.ts, src/db/mutations/note-labels.ts, src/db/mutations/entity-links.ts, src/db/mutations/attachments.ts, src/db/mutations/attachments.server.ts, src/db/mutations/sync.ts, src/db/queries/notes.ts, src/db/queries/note-labels.ts, src/db/queries/note-links.ts, src/db/queries/attachments.ts, src/db/queries/linkable-resources.ts, src/hooks/live-sync-provider.tsx, src/server/uploadthing.ts
Implements CRUD server functions with optimistic React Query cache updates for notes and note labels; adds link/unlink mutations; extends attachment handlers for note ownership; adds sync coarse-topic mappings and live-sync cache invalidation for note/note-label events; adds blob upload support.
Reusable taxonomy management components
src/components/taxonomy/TaxonomyColorPicker.tsx, src/components/taxonomy/TaxonomyListItem.tsx, src/components/taxonomy/TaxonomySortableList.tsx, src/components/taxonomy/TaxonomyManagementSection.tsx, src/components/taxonomy/types.ts, src/components/taxonomy/index.ts, src/components/forms/EntityConfigCreateForm.tsx, src/routes/_signed-in/labels.tsx, src/routes/_signed-in/statuses.tsx
Adds drag-sortable, inline-edit taxonomy list item and management section components; removes EntityList/EntityListItem; refactors Labels and Statuses pages to use the new taxonomy abstraction.
Note label selection, color editing, and management page
src/components/notes/NoteLabelColorPicker.tsx, src/components/notes/NoteEditableLabelBadge.tsx, src/components/notes/NoteLabelSelect.tsx, src/components/notes/NoteLabels.tsx, src/components/notes/use-note-label-color.ts, src/components/EntityFormSheets.tsx, src/lib/form-sheet-search.ts, src/routes/_signed-in/note-labels.tsx
Adds command-popover label picker with inline create/color edit, editable label badge, label assignment component, color update hook, NoteLabelCreateSheet, create-note-label sheet search param, and the Note Labels management route.
NoteEditor, attachments, and note sub-components
src/components/notes/NoteEditor.tsx, src/components/notes/styles.css, src/components/AttachmentListItem.tsx, src/components/AttachmentArea.tsx, src/components/notes/NoteArchivedBadge.tsx, src/components/notes/NoteLinkedResourceItem.tsx, src/components/notes/NoteLinkPicker.tsx, src/components/notes/CreateNoteButton.tsx, src/components/site-header.tsx
Adds TipTap editor with debounced save, formatting toolbar, and CSS; sidebar attachment list item; variant-aware AttachmentArea; archived badge; linked resource row with confirm-delete; searchable link picker; create-note button variants and Ctrl+Shift+N global shortcut.
Notes list page
src/components/notes/NoteListFilters.tsx, src/components/notes/NoteListRow.tsx, src/routes/_signed-in/notes.tsx, src/routes/_signed-in/notes.index.tsx
Adds filter chips for labels/attachments/archived, note list row with label assignment, notes layout route, and notes index route with search input, grouped sections, and empty states.
Note detail page
src/components/notes/NotePageMenu.tsx, src/components/notes/NoteSidebar.tsx, src/components/notes/NotePageContent.tsx, src/components/DetailSplitLayout.tsx, src/routes/_signed-in/notes.$noteId.tsx
Adds archive/delete menu, three-section sidebar (attachments/links/activity), split-layout detail page with drag-upload, title editor, and the /$noteId route with loader and fallback.
Route registration, navigation, and page layout
src/routeTree.gen.ts, src/components/app-sidebar.tsx, src/components/PageBackLink.tsx, src/components/PageLayout.tsx, src/components/PageSection.tsx, src/components/LabelSelect.tsx, src/components/nav-main.tsx, src/lib/page-layout-width.ts, src/routes/_signed-in/...
Registers Notes and Note Labels routes; adds sidebar nav entries; adds PageBackLink; extends PageLayout with width/header props; adds actions to PageSection; fixes nav active-state for top-level routes; sets width="full" on tasks/sprints/projects pages; minor integrations layout tweaks.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant NotesIndexRoute
  participant NoteListRow
  participant NotePageContent
  participant NoteEditor
  participant NoteSidebar
  participant ServerFn as Server Functions
  participant DB

  User->>NotesIndexRoute: navigates to /notes
  NotesIndexRoute->>ServerFn: fetchNotes, fetchNoteLabels
  ServerFn->>DB: query notes, note_labels
  ServerFn-->>NotesIndexRoute: NoteWithRelations[], NoteLabel[]
  NotesIndexRoute->>NoteListRow: renders each note row
  User->>NoteListRow: clicks note title
  NoteListRow->>NotePageContent: navigates to /notes/$noteId
  NotePageContent->>ServerFn: fetchNote, fetchNoteLinks
  ServerFn->>DB: query note + relations + entity_links
  ServerFn-->>NotePageContent: NoteWithRelations, NoteLinkedResource[]
  User->>NoteEditor: types body content
  NoteEditor->>NotePageContent: onSave after 500ms debounce
  NotePageContent->>ServerFn: updateNote(id, body, bodyPlain)
  ServerFn->>DB: update notes row
  ServerFn-->>NotePageContent: ok
  User->>NoteSidebar: drops file
  NoteSidebar->>ServerFn: createAttachment(noteId, file)
  ServerFn->>DB: insert attachment row
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

  • pnodev/project-y#17: Both PRs modify src/components/AttachmentArea.tsx to change upload/drag-and-drop handling and attachment rendering state.
  • pnodev/project-y#18: Both PRs modify src/components/PageLayout.tsx at the component signature and rendered layout structure level.
  • pnodev/project-y#24: Both PRs extend src/lib/form-sheet-search.ts by adding entries to the route-search schema and FORM_SHEET_CREATE_LINKS.

Poem

🐇 A rabbit once scribbled on leaves in the dell,
Now notes have a home where our thoughts love to dwell,
With labels of color and links to each task,
"What shall I write?" is all you need ask.
Ctrl+Shift+N — a new note appears!
The warren of features brings coding-day cheers. 🌿

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title is related to the change, but it is too vague to convey the main scope of the PR. Use a more specific title like "Add notes, labels, links, and note pages" to summarize the primary change.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch py-64-notes

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@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)
src/db/schema.ts (1)

348-370: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Enforce the attachment parent XOR at the database level.

The Zod refine below only protects the current server mutation path; the table itself still allows orphaned rows (task_id and note_id both null) and doubly-linked rows (both set). Since downstream code assumes an attachment belongs to exactly one parent, this needs a DB CHECK constraint, with the generated migration updated to match.

🤖 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 `@src/db/schema.ts` around lines 348 - 370, The attachments table currently
allows invalid parent combinations because only the Zod refine enforces the
taskId/noteId XOR in application code. Update the attachments definition in
schema.ts to add a database CHECK constraint on the attachment table so exactly
one of taskId or noteId is non-null, and make sure the generated migration
mirrors that constraint. Use the attachments/createTable definition and its
existing taskId/noteId columns to locate the change.
🟡 Minor comments (13)
src/components/notes/NoteLabelSelect.tsx-79-80 (1)

79-80: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Don't map every create failure to a duplicate-name error.

This catch also handles network/auth/server failures, so users can get a false duplicate warning for unrelated problems. Surface the actual error when available and fall back to a generic "Failed to create note label" message instead.

🤖 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 `@src/components/notes/NoteLabelSelect.tsx` around lines 79 - 80, The catch
block in NoteLabelSelect’s label-creation flow is mapping every failure to a
duplicate-name toast, which can misreport network/auth/server errors as name
conflicts. Update the create-label handler in NoteLabelSelect to capture the
thrown error from the label creation call, inspect it if available, and show the
real message when possible; otherwise fall back to a generic “Failed to create
note label” toast instead of always using the duplicate-name text.
src/lib/format-relative-time.ts-9-9 (1)

9-9: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use const for diffSec to satisfy lint and express immutability.

Line 9 declares diffSec with let, but it is never reassigned (prefer-const).

Suggested fix
-  let diffSec = Math.round((d.getTime() - now.getTime()) / 1000);
+  const diffSec = Math.round((d.getTime() - now.getTime()) / 1000);
🤖 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 `@src/lib/format-relative-time.ts` at line 9, `formatRelativeTime` computes
`diffSec` once and never reassigns it, so change the declaration in
`format-relative-time.ts` from a mutable binding to an immutable one. Update the
`diffSec` variable in `formatRelativeTime` to use `const` so it aligns with the
`prefer-const` lint rule and matches the intended one-time calculation.

Source: Linters/SAST tools

src/db/queries/linkable-resources.ts-61-107 (1)

61-107: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Don't double-limit the per-type search results.

Tasks and projects are each queried with limit: SEARCH_LIMIT, then concatenated with tasks first and truncated again. Once 20 task matches exist, every project match disappears, which makes some valid link targets unreachable from the picker. Apply the cap after a single merged ranking, or reserve slots per type explicitly.

🤖 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 `@src/db/queries/linkable-resources.ts` around lines 61 - 107, The search in
linkable-resources.ts is applying SEARCH_LIMIT separately to tasks and projects
and then truncating the combined list in filterUnlinkedResources, which can
starve project results when task matches are abundant. Update the logic around
the matchedTasks/matchedProjects Promise.all flow and the resources aggregation
so the cap is enforced after a single merged result set or by reserving explicit
slots per type, ensuring both task and project matches can surface in the
picker.
src/lib/notes/note-activity.ts-36-43 (1)

36-43: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Don't hide real edits made within the first minute.

This collapses any save within 60 seconds of creation into the "created" event, so legitimate early edits never appear in the activity feed. The safe dedupe case is identical timestamps, not an arbitrary one-minute window.

Proposed fix
-  if (Math.abs(updatedAt.getTime() - createdAt.getTime()) > 60_000) {
+  if (updatedAt.getTime() !== createdAt.getTime()) {
🤖 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 `@src/lib/notes/note-activity.ts` around lines 36 - 43, The activity dedupe
logic in note-activity should not use a 60-second window to suppress updates,
because that hides real early edits from the feed. Update the condition in the
code that builds the items list so it only skips the “updated” event when
updatedAt and createdAt are identical, and keep the “Note updated” entry for any
real timestamp difference. Refer to the note activity builder around the
updatedAt/createdAt comparison to make the change.
src/db/mutations/note-labels.ts-143-154 (1)

143-154: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Patch or invalidate note-labels-with-counts here too.

The note-label management screen reads from useNoteLabelsWithCountsQuery() in src/routes/_signed-in/note-labels.tsx:33-114, but this mutation only updates ["note-labels"] and note caches. After a rename/color change, that page can keep showing stale label data until an external refetch happens.

Also applies to: 183-188

🤖 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 `@src/db/mutations/note-labels.ts` around lines 143 - 154, The label mutation
update path only refreshes note-label caches, so the note-label management
screen using useNoteLabelsWithCountsQuery can stay stale after a rename or color
change. In the mutation handlers in note-labels.ts, update the same query cache
key used by note-labels-with-counts as well as the existing ["note-labels"] and
["notes"] entries, or invalidate that query after the mutation succeeds so the
UI refetches fresh data. Apply this in both relevant update flows in the
note-label mutation module.
src/db/mutations/attachments.ts-50-56 (1)

50-56: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Use the same sync payload shape for task updates.

Line 52 sends { taskId }, while note updates here and task updates from attachment deletion use { id }. Align this to avoid consumers missing the changed entity ID.

Proposed fix
     await sync("attachment-create", { data });
     if (data.taskId) {
-      await sync(`task-update-${data.taskId}`, { data: { taskId: data.taskId } });
+      await sync(`task-update-${data.taskId}`, { data: { id: data.taskId } });
     }
🤖 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 `@src/db/mutations/attachments.ts` around lines 50 - 56, The task update sync
payload in the attachment mutation is inconsistent with the rest of the code and
should use the same ID shape as note updates and attachment deletion. Update the
payload passed to sync in the attachment create path inside the attachment
mutation logic so task updates send the changed entity identifier as id,
matching the existing sync contract used by the task-update and note-update
flows.
src/db/mutations/notes.ts-124-135 (1)

124-135: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Mirror server-derived titles in the optimistic cache.

Line 126 stores the raw noteData.title, but the server derives title from data.title/bodyPlain on Lines 84-89. Clearing a title with existing body text can leave the client cache with null while the persisted note has a derived title.

Proposed fix
       const patchNote = (entry: NoteWithRelations): NoteWithRelations => ({
         ...entry,
-        ...(noteData.title !== undefined ? { title: noteData.title } : {}),
+        ...(noteData.title !== undefined
+          ? {
+              title: deriveNoteTitle(
+                noteData.title,
+                noteData.bodyPlain ?? entry.bodyPlain
+              ),
+            }
+          : {}),
         ...(noteData.body !== undefined ? { body: noteData.body } : {}),
🤖 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 `@src/db/mutations/notes.ts` around lines 124 - 135, Update the optimistic
cache in patchNote so it mirrors the server-side title derivation used in the
note mutation flow. Instead of blindly applying noteData.title, compute the
cached title the same way the create/update logic does by deriving it from
data.title/bodyPlain when title is absent or cleared. Keep patchNote in sync
with the server’s title handling so NoteWithRelations stays consistent after
optimistic updates.
src/db/mutations/entity-links.ts-61-101 (1)

61-101: 🗄️ Data Integrity & Integration | 🟡 Minor

Handle the concurrent duplicate-link race

entityLinks already has a unique index on (owner, sourceType, sourceId, targetType, targetId, linkKind), so duplicate rows are blocked. The read-before-insert path still races under concurrent requests, though, and the loser can fail with a unique-violation instead of returning the existing linkId. Use ON CONFLICT DO NOTHING/RETURNING or catch the conflict and fetch the row.

🤖 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 `@src/db/mutations/entity-links.ts` around lines 61 - 101, The existing
read-before-insert flow in entity-links.ts can race under concurrent requests,
causing a unique-violation instead of returning the already-created linkId.
Update the logic around the existing lookup and the insert in the entity-link
mutation to use an atomic insert with conflict handling (for the unique key on
entityLinks) or, if the insert conflicts, immediately query and return the
existing row’s id. Keep the returned shape from the mutation unchanged.
src/components/AttachmentArea.tsx-45-53 (1)

45-53: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

This effect closes the panel dropzone after upload errors.

In the compact/sidebar flow, onUploadError sets showUpload(true), but this effect runs as soon as isUploading returns to false and immediately resets it to false again. That collapses the retry UI right after a failed upload.

🤖 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 `@src/components/AttachmentArea.tsx` around lines 45 - 53, The AttachmentArea
useEffect is overriding the compact/sidebar retry state after an upload failure
by forcing setShowUpload(false) whenever isUploading becomes false. Update this
effect so the panel dropzone stays open after onUploadError sets
showUpload(true), preserving the retry UI in the panel flow while still
switching view based on attachments.length and isPanelVariant. Use the existing
AttachmentArea useEffect, setShowUpload, setView, and onUploadError flow to gate
the reset logic instead of unconditionally closing the panel.
src/components/notes/NoteListFilters.tsx-54-71 (1)

54-71: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

All never becomes active while a search is present.

isNoteListFilterActive() counts searchQuery, but clearFilters() deliberately preserves it. That leaves the “All” chip visually inactive even after clicking it, which is confusing for the only clear/reset affordance in this group. Either clear searchQuery here or derive the chip state from chip-only filters.

Also applies to: 103-116

🤖 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 `@src/components/notes/NoteListFilters.tsx` around lines 54 - 71, The “All”
chip state is still being driven by `isNoteListFilterActive()` even though
`clearFilters()` keeps `filters.searchQuery`, so it can remain visually inactive
after reset. Update `NoteListFilters` so the All chip’s active state is based
only on chip-controlled filters (or alternatively clear `searchQuery` in
`clearFilters()`), and ensure the logic in `toggleLabel`/`clearFilters` matches
that behavior consistently.
src/components/site-header.tsx-39-49 (1)

39-49: 🎯 Functional Correctness | 🟡 Minor

Normalize the shortcut key check.

event.key === "n" is brittle here because the shortcut includes Shift; on common layouts this yields "N", so the handler can miss the intended chord. Use event.code === "KeyN" or normalize with event.key.toLowerCase().

🤖 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 `@src/components/site-header.tsx` around lines 39 - 49, The keyboard shortcut
check in useEffect is too brittle because it compares event.key directly while
Shift is pressed, which can change the reported value and miss the chord. Update
the handler in site-header.tsx to normalize the key comparison, either by using
event.code for the N key or by lowercasing event.key before checking it, while
keeping the existing meta/ctrl and shift modifiers intact in createNoteShortcut.
src/components/PageLayout.tsx-29-45 (1)

29-45: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Only render the actions container when actions exists.

Line 43 still mounts an empty wrapper on title-only pages, so the mt-4/md:ml-4 spacing applies even when there is nothing to show on the right.

Suggested fix
-          <div className="mt-4 flex shrink-0 items-center gap-2 md:mt-0 md:ml-4">
-            {actions ? actions : null}
-          </div>
+          {actions ? (
+            <div className="mt-4 flex shrink-0 items-center gap-2 md:mt-0 md:ml-4">
+              {actions}
+            </div>
+          ) : null}
🤖 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 `@src/components/PageLayout.tsx` around lines 29 - 45, The actions wrapper in
PageLayout should only be rendered when there are actual actions to show,
because the always-mounted container still applies spacing on title-only pages.
Update the conditional around the actions area in PageLayout so the right-side
div is created only when actions is present, while keeping the existing
header/title rendering unchanged.
src/routes/_signed-in/notes.index.tsx-93-108 (1)

93-108: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Archived-only workspaces hit the wrong empty state.

Line 93 treats “no active notes” as “no notes at all”, so a user with only archived notes sees “No notes yet” even though notes already exist.

Possible fix
-      {!hasNotes && !filters.showArchived ? (
+      {notesQuery.data.length === 0 ? (
         <div className="text-muted-foreground flex flex-col items-center gap-3 py-16 text-center">
           <StickyNote className="size-10 opacity-40" />
           <p className="text-sm">
             No notes yet. Create one to capture a quick thought.
           </p>
         </div>
+      ) : !hasNotes && hasArchivedNotes && !filters.showArchived && !isFilterActive ? (
+        <div className="text-muted-foreground flex flex-col items-center gap-3 py-16 text-center">
+          <StickyNote className="size-10 opacity-40" />
+          <p className="text-sm">All notes are archived. Turn on Archived to view them.</p>
+        </div>
       ) : filteredNotes.length === 0 ? (
🤖 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 `@src/routes/_signed-in/notes.index.tsx` around lines 93 - 108, Update the
empty-state branching in notes.index.tsx so “No notes yet” only shows when there
are truly no notes, not merely no active notes. In the main conditional around
the empty state, use the existing symbols hasNotes, filteredNotes.length, and
filters.showArchived to distinguish archived-only workspaces from genuinely
empty ones, and ensure archived-only users fall through to the archived-related
empty state instead of the “No notes yet” message.
🧹 Nitpick comments (2)
src/components/taxonomy/TaxonomyManagementSection.tsx (1)

66-70: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Forward createLink.params through the shared create button.

CreateLink includes params, but this wrapper drops them. That makes the abstraction unusable for any taxonomy route whose create destination needs path params.

Suggested fix
         <PageCreateButton
           label={createButtonLabel}
           to={createLink.to}
           search={createLink.search}
+          params={createLink.params}
         />
🤖 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 `@src/components/taxonomy/TaxonomyManagementSection.tsx` around lines 66 - 70,
Forward the missing create-route path params from TaxonomyManagementSection
through PageCreateButton, since the shared create button currently passes only
to and search and drops createLink.params. Update the TaxonomyManagementSection
wrapper to include params when rendering PageCreateButton, and make sure
PageCreateButton accepts and forwards params so create destinations that require
route parameters still work.
src/routes/_signed-in/notes.$noteId.tsx (1)

18-20: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Avoid re-fetching the note in the loader.

You already load this query in ensureQueryData; use that result for pageTitle instead of calling fetchQuery again.

Proposed change
   loader: async ({ context, params }) => {
-    await Promise.all([
-      context.queryClient.ensureQueryData(noteQueryOptions(params.noteId)),
+    const [note] = await Promise.all([
+      context.queryClient.ensureQueryData(noteQueryOptions(params.noteId)),
       context.queryClient.ensureQueryData(noteLinksQueryOptions(params.noteId)),
       context.queryClient.ensureQueryData(noteLabelsQueryOptions()),
     ]);
-
-    const note = await context.queryClient.fetchQuery(
-      noteQueryOptions(params.noteId)
-    );
 
     return {
       pageTitle: note?.title || "Untitled note",
🤖 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 `@src/routes/_signed-in/notes`.$noteId.tsx around lines 18 - 20, The loader is
fetching the same note twice, once via ensureQueryData and again via fetchQuery
for pageTitle. Update the notes.$noteId loader to reuse the result already
returned by ensureQueryData, and reference that value when building pageTitle
instead of calling fetchQuery again in the same loader.
🤖 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 `@src/components/AttachmentListItem.tsx`:
- Around line 114-117: The AttachmentListItem deletion flow leaves isDeleting
stuck true if deleteAttachment(attachment.id) throws, so move the state reset
into a finally block around the async onConfirm handler. Update the onConfirm
logic in AttachmentListItem to setIsDeleting(true) before calling
deleteAttachment and always call setIsDeleting(false) afterward, regardless of
success or failure.

In `@src/components/notes/CreateNoteButton.tsx`:
- Around line 101-107: The useCreateNoteShortcut callback is currently
re-entrant, so repeated keydown events can trigger createEmptyNote multiple
times before navigation completes. Add a single-flight guard inside
useCreateNoteShortcut, using a ref or in-flight promise to ignore subsequent
invocations until the current createEmptyNote(createNote, navigate) call
finishes, while keeping the existing createNote and navigate dependencies
intact.

In `@src/components/notes/NoteEditor.tsx`:
- Around line 176-217: The icon-only toolbar controls in NoteEditor are missing
accessible names, so add an aria-label and ideally a title to each MenuBarButton
in the formatting toolbar. Update the button entries for bold, italic,
strikethrough, bullet list, ordered list, code block, and blockquote so
assistive tech can identify each action, using the existing button handlers in
NoteEditor and the MenuBarButton component.
- Around line 84-99: The debounced save in NoteEditor’s onUpdate is skipping the
first user edit after mount or note changes because the hasInitialized guard
returns early, leaving the initial body change unsaved if the user stops typing.
Update the NoteEditor onUpdate/hasInitialized flow so the first
post-initialization edit still schedules the save debounce, while only
suppressing the programmatic update from loading a note; use the existing
onUpdate, hasInitialized, debounceRef, and onSaveRef logic to distinguish those
cases.

In `@src/components/notes/NotePageContent.tsx`:
- Around line 35-58: The local title state in NotePageContent is only
initialized from the incoming note once, so when the note prop changes it can
keep a stale value and be reused by handleSaveBody when calling updateNote.
Update NotePageContent to synchronize the title state with note.title whenever
the note changes, and keep the existing save flow in
handleSaveBody/useUpdateNoteMutation using the current note.id and trimmed
title.
- Around line 74-100: The handleUpload callback is passing nullable upload
metadata straight into a required database field, so remove the unsafe cast on
attachment.serverData.uploadedBy and ensure createAttachment always receives a
non-null uploadedBy value. Use the existing handleUpload flow in NotePageContent
and either skip attachments with a null uploadedBy or map them to a valid
fallback before calling createAttachment, so the persisted uploadedBy column
never receives null.

In `@src/components/notes/use-note-label-color.ts`:
- Around line 4-22: The color-only label update helper is sending a full
snapshot instead of just the changed field, which can overwrite newer name/order
edits. Update toNoteLabelUpdate and the useUpdateNoteLabelColor flow so the
mutation payload only includes the label id and the new color, and keep the
early return logic in useUpdateNoteLabelColor intact.

In `@src/components/taxonomy/TaxonomyColorPicker.tsx`:
- Around line 16-45: The current TaxonomyColorPicker interaction uses
listbox/option roles on standalone buttons, which does not match the implemented
keyboard behavior. Update the component to use radiogroup/radio semantics around
the COLOR_VALUES map and the button items, or alternatively add full listbox
keyboard handling with roving focus if you keep listbox roles. Make sure the
selection state and disabled handling stay wired through the existing onChange,
value, and disabled props in TaxonomyColorPicker.

In `@src/components/taxonomy/TaxonomyListItem.tsx`:
- Around line 113-124: The icon-only Button controls in TaxonomyListItem lack
accessible names, so add a clear accessible label to each affected Button
(including the grip handle and the other icon-only action around the second
occurrence) using an aria-label or equivalent accessible text. Update the Button
usages in TaxonomyListItem so assistive technologies can identify the action,
while keeping the existing icon rendering and drag/listener behavior intact.

In `@src/components/taxonomy/TaxonomySortableList.tsx`:
- Around line 39-45: handleDragEnd in TaxonomySortableList currently treats a
missing drop target as a valid reorder, so cancelled or outside drops can still
persist changes. Update the DragEndEvent handling to return early when over is
null, and also guard against missing item positions by checking the oldIndex and
newIndex before calling onReorder(arrayMove(...)). Keep the fix localized to
handleDragEnd and use the existing sortedItems lookup logic.

In `@src/db/mutations/attachments.ts`:
- Around line 22-42: The attachment create flow in the mutations handler should
strictly validate parent ownership before inserting. Update the attachment
creation logic in the mutation that checks data.taskId and data.noteId so it
rejects requests with neither ID, rejects requests with both IDs, and does not
silently return when the parent task/note lookup fails. Use the existing
db.query.tasks.findFirst, db.query.notes.findFirst, and db.insert(attachments)
path to enforce a single valid parent and surface an explicit error instead of
leaving uploaded files orphaned.

In `@src/db/mutations/note-labels.ts`:
- Around line 196-224: The bulk update handler in updateMultipleNoteLabels
currently bypasses the same case-insensitive name-uniqueness validation enforced
by createNoteLabel and updateNoteLabel, so add that check before applying any
updates. In the updateMultipleNoteLabels createServerFn flow, validate the
incoming data against existing noteLabels for the session.user.id and reject any
duplicate name (excluding the label being updated) before the Promise.all
update/sync steps run. Reuse the same normalization/duplicate-detection logic as
the single-label mutation paths so the bulk path preserves the same server-side
invariant.

In `@src/db/mutations/notes.ts`:
- Around line 202-214: The note deletion flow in the delete mutation currently
removes the row before calling deleteAttachmentFilesFromStorage, so a storage
failure leaves orphaned files and makes retries fail with “Note not found.”
Update the note deletion path in the mutation around the db.delete(notes) and
deleteAttachmentFilesFromStorage calls to make attachment cleanup best-effort,
such as catching and logging cleanup errors after the row is deleted, or
queueing cleanup into a durable/background process instead of failing the whole
operation.
- Around line 271-282: The note label replacement logic in the mutation should
be atomic because the current delete-then-insert flow can leave partial state
and allow concurrent interleaving. Update the note label update path in the
relevant notes mutation function to run the delete and optional insert together
inside a single transaction on the same db instance, so the replacement either
fully succeeds or fully रोलs back. Use the existing note label replacement block
that deletes from noteLabelsToNotes and inserts via data.labelIds as the place
to wrap with a transaction.

In `@src/db/schema.ts`:
- Around line 278-289: The join table schema in noteLabelsToNotes only has the
composite primary key, so deletes and lookups by noteId are not indexed. Add a
dedicated note_id-leading index to the note_labels_to_notes table definition in
schema.ts, and make sure the corresponding migration creates that index as well
so the delete path in notes mutations can use it efficiently.
- Around line 272-275: The note-label unique constraint in the schema does not
match the case-insensitive behavior enforced by createNoteLabel, so duplicates
like "Bug" and "bug" can still slip through under concurrency. Update the
NoteLabel index definition in schema.ts to enforce uniqueness on a
normalized/lowercased name per user, and make the companion migration/backfill
apply the same case-insensitive rule. Use the existing createNoteLabel logic and
the note_label_user_name_unique index as the anchor points when updating both
schema and migration.

---

Outside diff comments:
In `@src/db/schema.ts`:
- Around line 348-370: The attachments table currently allows invalid parent
combinations because only the Zod refine enforces the taskId/noteId XOR in
application code. Update the attachments definition in schema.ts to add a
database CHECK constraint on the attachment table so exactly one of taskId or
noteId is non-null, and make sure the generated migration mirrors that
constraint. Use the attachments/createTable definition and its existing
taskId/noteId columns to locate the change.

---

Minor comments:
In `@src/components/AttachmentArea.tsx`:
- Around line 45-53: The AttachmentArea useEffect is overriding the
compact/sidebar retry state after an upload failure by forcing
setShowUpload(false) whenever isUploading becomes false. Update this effect so
the panel dropzone stays open after onUploadError sets showUpload(true),
preserving the retry UI in the panel flow while still switching view based on
attachments.length and isPanelVariant. Use the existing AttachmentArea
useEffect, setShowUpload, setView, and onUploadError flow to gate the reset
logic instead of unconditionally closing the panel.

In `@src/components/notes/NoteLabelSelect.tsx`:
- Around line 79-80: The catch block in NoteLabelSelect’s label-creation flow is
mapping every failure to a duplicate-name toast, which can misreport
network/auth/server errors as name conflicts. Update the create-label handler in
NoteLabelSelect to capture the thrown error from the label creation call,
inspect it if available, and show the real message when possible; otherwise fall
back to a generic “Failed to create note label” toast instead of always using
the duplicate-name text.

In `@src/components/notes/NoteListFilters.tsx`:
- Around line 54-71: The “All” chip state is still being driven by
`isNoteListFilterActive()` even though `clearFilters()` keeps
`filters.searchQuery`, so it can remain visually inactive after reset. Update
`NoteListFilters` so the All chip’s active state is based only on
chip-controlled filters (or alternatively clear `searchQuery` in
`clearFilters()`), and ensure the logic in `toggleLabel`/`clearFilters` matches
that behavior consistently.

In `@src/components/PageLayout.tsx`:
- Around line 29-45: The actions wrapper in PageLayout should only be rendered
when there are actual actions to show, because the always-mounted container
still applies spacing on title-only pages. Update the conditional around the
actions area in PageLayout so the right-side div is created only when actions is
present, while keeping the existing header/title rendering unchanged.

In `@src/components/site-header.tsx`:
- Around line 39-49: The keyboard shortcut check in useEffect is too brittle
because it compares event.key directly while Shift is pressed, which can change
the reported value and miss the chord. Update the handler in site-header.tsx to
normalize the key comparison, either by using event.code for the N key or by
lowercasing event.key before checking it, while keeping the existing meta/ctrl
and shift modifiers intact in createNoteShortcut.

In `@src/db/mutations/attachments.ts`:
- Around line 50-56: The task update sync payload in the attachment mutation is
inconsistent with the rest of the code and should use the same ID shape as note
updates and attachment deletion. Update the payload passed to sync in the
attachment create path inside the attachment mutation logic so task updates send
the changed entity identifier as id, matching the existing sync contract used by
the task-update and note-update flows.

In `@src/db/mutations/entity-links.ts`:
- Around line 61-101: The existing read-before-insert flow in entity-links.ts
can race under concurrent requests, causing a unique-violation instead of
returning the already-created linkId. Update the logic around the existing
lookup and the insert in the entity-link mutation to use an atomic insert with
conflict handling (for the unique key on entityLinks) or, if the insert
conflicts, immediately query and return the existing row’s id. Keep the returned
shape from the mutation unchanged.

In `@src/db/mutations/note-labels.ts`:
- Around line 143-154: The label mutation update path only refreshes note-label
caches, so the note-label management screen using useNoteLabelsWithCountsQuery
can stay stale after a rename or color change. In the mutation handlers in
note-labels.ts, update the same query cache key used by note-labels-with-counts
as well as the existing ["note-labels"] and ["notes"] entries, or invalidate
that query after the mutation succeeds so the UI refetches fresh data. Apply
this in both relevant update flows in the note-label mutation module.

In `@src/db/mutations/notes.ts`:
- Around line 124-135: Update the optimistic cache in patchNote so it mirrors
the server-side title derivation used in the note mutation flow. Instead of
blindly applying noteData.title, compute the cached title the same way the
create/update logic does by deriving it from data.title/bodyPlain when title is
absent or cleared. Keep patchNote in sync with the server’s title handling so
NoteWithRelations stays consistent after optimistic updates.

In `@src/db/queries/linkable-resources.ts`:
- Around line 61-107: The search in linkable-resources.ts is applying
SEARCH_LIMIT separately to tasks and projects and then truncating the combined
list in filterUnlinkedResources, which can starve project results when task
matches are abundant. Update the logic around the matchedTasks/matchedProjects
Promise.all flow and the resources aggregation so the cap is enforced after a
single merged result set or by reserving explicit slots per type, ensuring both
task and project matches can surface in the picker.

In `@src/lib/format-relative-time.ts`:
- Line 9: `formatRelativeTime` computes `diffSec` once and never reassigns it,
so change the declaration in `format-relative-time.ts` from a mutable binding to
an immutable one. Update the `diffSec` variable in `formatRelativeTime` to use
`const` so it aligns with the `prefer-const` lint rule and matches the intended
one-time calculation.

In `@src/lib/notes/note-activity.ts`:
- Around line 36-43: The activity dedupe logic in note-activity should not use a
60-second window to suppress updates, because that hides real early edits from
the feed. Update the condition in the code that builds the items list so it only
skips the “updated” event when updatedAt and createdAt are identical, and keep
the “Note updated” entry for any real timestamp difference. Refer to the note
activity builder around the updatedAt/createdAt comparison to make the change.

In `@src/routes/_signed-in/notes.index.tsx`:
- Around line 93-108: Update the empty-state branching in notes.index.tsx so “No
notes yet” only shows when there are truly no notes, not merely no active notes.
In the main conditional around the empty state, use the existing symbols
hasNotes, filteredNotes.length, and filters.showArchived to distinguish
archived-only workspaces from genuinely empty ones, and ensure archived-only
users fall through to the archived-related empty state instead of the “No notes
yet” message.

---

Nitpick comments:
In `@src/components/taxonomy/TaxonomyManagementSection.tsx`:
- Around line 66-70: Forward the missing create-route path params from
TaxonomyManagementSection through PageCreateButton, since the shared create
button currently passes only to and search and drops createLink.params. Update
the TaxonomyManagementSection wrapper to include params when rendering
PageCreateButton, and make sure PageCreateButton accepts and forwards params so
create destinations that require route parameters still work.

In `@src/routes/_signed-in/notes`.$noteId.tsx:
- Around line 18-20: The loader is fetching the same note twice, once via
ensureQueryData and again via fetchQuery for pageTitle. Update the notes.$noteId
loader to reuse the result already returned by ensureQueryData, and reference
that value when building pageTitle instead of calling fetchQuery again in the
same loader.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 9425b13b-6d44-4400-b354-c2e073d4bb95

📥 Commits

Reviewing files that changed from the base of the PR and between 339c681 and 92c7c8a.

📒 Files selected for processing (82)
  • drizzle/0007_chemical_speedball.sql
  • drizzle/0008_note_labels.sql
  • drizzle/0009_note_archive.sql
  • drizzle/meta/0007_snapshot.json
  • drizzle/meta/0008_snapshot.json
  • drizzle/meta/0009_snapshot.json
  • drizzle/meta/_journal.json
  • src/components/AttachmentArea.tsx
  • src/components/AttachmentListItem.tsx
  • src/components/DetailSplitLayout.tsx
  • src/components/EntityFormSheets.tsx
  • src/components/EntityList.tsx
  • src/components/LabelSelect.tsx
  • src/components/PageBackLink.tsx
  • src/components/PageLayout.tsx
  • src/components/PageSection.tsx
  • src/components/app-sidebar.tsx
  • src/components/forms/EntityConfigCreateForm.tsx
  • src/components/nav-main.tsx
  • src/components/notes/CreateNoteButton.tsx
  • src/components/notes/NoteArchivedBadge.tsx
  • src/components/notes/NoteEditableLabelBadge.tsx
  • src/components/notes/NoteEditor.tsx
  • src/components/notes/NoteLabelColorPicker.tsx
  • src/components/notes/NoteLabelSelect.tsx
  • src/components/notes/NoteLabels.tsx
  • src/components/notes/NoteLinkPicker.tsx
  • src/components/notes/NoteLinkedResourceItem.tsx
  • src/components/notes/NoteListFilters.tsx
  • src/components/notes/NoteListRow.tsx
  • src/components/notes/NotePageContent.tsx
  • src/components/notes/NotePageMenu.tsx
  • src/components/notes/NoteSidebar.tsx
  • src/components/notes/styles.css
  • src/components/notes/use-note-label-color.ts
  • src/components/site-header.tsx
  • src/components/taxonomy/TaxonomyColorPicker.tsx
  • src/components/taxonomy/TaxonomyListItem.tsx
  • src/components/taxonomy/TaxonomyManagementSection.tsx
  • src/components/taxonomy/TaxonomySortableList.tsx
  • src/components/taxonomy/index.ts
  • src/components/taxonomy/types.ts
  • src/db/mutations/attachments.server.ts
  • src/db/mutations/attachments.ts
  • src/db/mutations/entity-links.ts
  • src/db/mutations/note-labels.ts
  • src/db/mutations/notes.ts
  • src/db/mutations/sync.ts
  • src/db/queries/attachments.ts
  • src/db/queries/linkable-resources.ts
  • src/db/queries/note-labels.ts
  • src/db/queries/note-links.ts
  • src/db/queries/notes.ts
  • src/db/schema.ts
  • src/hooks/live-sync-provider.tsx
  • src/lib/form-sheet-search.ts
  • src/lib/format-relative-time.test.ts
  • src/lib/format-relative-time.ts
  • src/lib/notes/derive-title.test.ts
  • src/lib/notes/derive-title.ts
  • src/lib/notes/entity-link-utils.ts
  • src/lib/notes/filter-notes.test.ts
  • src/lib/notes/filter-notes.ts
  • src/lib/notes/linkable-resources.test.ts
  • src/lib/notes/linkable-resources.ts
  • src/lib/notes/list-preview.test.ts
  • src/lib/notes/list-preview.ts
  • src/lib/notes/note-activity.test.ts
  • src/lib/notes/note-activity.ts
  • src/lib/page-layout-width.ts
  • src/routeTree.gen.ts
  • src/routes/_signed-in/labels.tsx
  • src/routes/_signed-in/note-labels.tsx
  • src/routes/_signed-in/notes.$noteId.tsx
  • src/routes/_signed-in/notes.index.tsx
  • src/routes/_signed-in/notes.tsx
  • src/routes/_signed-in/projects.$projectId.tasks.tsx
  • src/routes/_signed-in/settings.integrations.tsx
  • src/routes/_signed-in/sprints.$sprintId.tasks.tsx
  • src/routes/_signed-in/statuses.tsx
  • src/routes/_signed-in/tasks.tsx
  • src/server/uploadthing.ts
💤 Files with no reviewable changes (1)
  • src/components/EntityList.tsx

Comment thread src/components/AttachmentListItem.tsx Outdated
Comment thread src/components/notes/CreateNoteButton.tsx
Comment thread src/components/notes/NoteEditor.tsx
Comment thread src/components/notes/NoteEditor.tsx
Comment thread src/components/notes/NotePageContent.tsx
Comment thread src/db/mutations/note-labels.ts
Comment thread src/db/mutations/notes.ts
Comment thread src/db/mutations/notes.ts Outdated
Comment thread src/db/schema.ts
Comment on lines +272 to +275
(example) => [
index("note_label_user_idx").on(example.userId),
uniqueIndex("note_label_user_name_unique").on(example.userId, example.name),
]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Make note-label uniqueness match the app's case-insensitive contract.

createNoteLabel treats "Bug" and "bug" as duplicates, but this unique index does not. Two concurrent requests can both pass the lower(name) pre-check and commit case-variant duplicates that the rest of the app considers the same label. Please enforce the uniqueness rule case-insensitively here and in the companion migration/backfill.

🤖 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 `@src/db/schema.ts` around lines 272 - 275, The note-label unique constraint in
the schema does not match the case-insensitive behavior enforced by
createNoteLabel, so duplicates like "Bug" and "bug" can still slip through under
concurrency. Update the NoteLabel index definition in schema.ts to enforce
uniqueness on a normalized/lowercased name per user, and make the companion
migration/backfill apply the same case-insensitive rule. Use the existing
createNoteLabel logic and the note_label_user_name_unique index as the anchor
points when updating both schema and migration.

Comment thread src/db/schema.ts
@kaesetoast kaesetoast merged commit 697121a into main Jun 24, 2026
5 checks passed
@github-actions

Copy link
Copy Markdown

🎉 This PR is included in version 0.14.0 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant