Skip to content

feat(ui): add dialog for review ref, tag, add TOC,...#274

Merged
NTGNguyen merged 44 commits into
mainfrom
ntgnguyen/doing-5
May 30, 2026
Merged

feat(ui): add dialog for review ref, tag, add TOC,...#274
NTGNguyen merged 44 commits into
mainfrom
ntgnguyen/doing-5

Conversation

@NTGNguyen
Copy link
Copy Markdown
Contributor

@NTGNguyen NTGNguyen commented May 30, 2026

Summary by CodeRabbit

  • New Features

    • Search results now show highlights and content snippets; note suggestion menu and tag previews added
    • Note reference previews and an inline note preview modal
    • Table of contents navigation and a note suggestion dropdown
    • Workspace member add-via-search and in-app toasts for workspace actions
  • Improvements

    • Redesigned landing pages and navigation; improved workspace sidebar and member management UX
    • Auth/client initialisation and sign-in/out flows refined; Meilisearch host exposure improved
    • Revision workflow UX and trash/restore UI polished
  • Bug Fixes

    • Fixed folder-parent recursion and Docker ignore behaviour; environment variable name corrected

Review Change Stack

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 30, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 4a0edd9f-e180-466b-a281-441fe6a2fca8

📥 Commits

Reviewing files that changed from the base of the PR and between 8a9e368 and 1ab6594.

📒 Files selected for processing (4)
  • apps/document/src/hocuspocus/hocuspocus.ts
  • packages/ui/src/hooks/use-editor-state.ts
  • packages/ui/src/hooks/use-is-doc-modified.ts
  • packages/ui/src/lib/auth-client.ts

📝 Walkthrough

Walkthrough

This PR refactors authentication client initialization from eager to lazy-loaded configuration, consolidates environment setup with Authentik integration, enhances Meilisearch with formatted result highlighting, introduces workspace SSE events for real-time updates, adds interactive reference/tag previews and document viewer mode, enables orphan node filtering in graph views, and redesigns the landing page with updated branding.

Changes

Auth, environment, search, events, and workspace features

Layer / File(s) Summary
Auth client lazy initialization
packages/ui/src/lib/auth-client.ts, packages/ui/src/lib/get-access-token-client-side.ts, packages/ui/src/components/signin-form.tsx, packages/ui/src/components/workspace-sidebar.tsx
Refactors authClient from module-level singleton to lazy-loaded, configurable client via configureAuthClient(baseURL) and getAuthClient(). All consumers switch to getAuthClient() calls.
Environment initialisation and TypeScript types
apps/web/app/env-init.tsx, apps/web/app/layout.tsx, apps/web/global.d.ts, apps/web/.env, apps/document/.env
Introduces EnvInit client component that configures auth once on startup. Adds global TypeScript ProcessEnv interface for auth and Authentik variables. Defines API_URL, Authentik credentials, and MEILISEARCH_HOST in environment files.
Auth and token import consolidation
apps/web/lib/auth.ts, apps/web/lib/get-access-token.ts, apps/web/proxy.ts, apps/web/tsconfig.json, apps/web/app/(workspace)/workspace/**/*, apps/web/app/api/auth/[...all]/route.ts
Moves fetchAccessTokenServerSide and auth imports from @ui/lib to local @lib/ paths. Updates TypeScript path aliases to support @lib/* pointing to ./lib/*. Consolidates auth module logic into web app.
Meilisearch result formatting and highlight parsing
packages/ui/src/block-note/menu.ts
Adds HIGHLIGHT_PRE_TAG/HIGHLIGHT_POST_TAG constants and parseHighlight() utility to segment formatted text. Extends SearchResult with optional formattedName and contentSnippet. Updates searchNotesFromMeilisearch and searchTagsFromMeilisearch to require non-null clients and return formatted results with highlights and content previews. Adds searchNotesByTag() function.
Reference and tag preview components
packages/ui/src/block-note/reference.tsx, packages/ui/src/block-note/tag.tsx
Adds ReferencePreview component for WebSocket-backed note viewing in modal. Adds TagPreview dialog that queries notes by tag and enables navigation. Both components accept optional apiUrl parameter and enable interactive inline previews with WebSocket viewers.
Workspace SSE event provider
packages/ui/src/contexts/workspace-events-context.tsx, packages/ui/src/index.ts
Introduces WorkspaceEventsProvider that manages SSE connection, fans out events to subscribers, and cleans up on unmount. Adds useWorkspaceEvents hook for component subscriptions. Re-exports context from UI package.
Tree view workspace event integration
packages/ui/src/components/tree-view.tsx
Replaces SSE effect with useWorkspaceEvents subscription that invalidates tree query on workspace updates. Moves note navigation from selection handler to per-item onClick/onKeyDown, enabling direct routing without selection-based flow.
Workspace content and member management
packages/ui/src/components/workspace-content-wrapper.tsx, apps/web/app/(workspace)/workspace/[workspaceId]/layout.tsx, packages/ui/src/components/workspace-members-modal.tsx
WorkspaceContentWrapper accepts optional meilisearchHost prop and wraps children in WorkspaceEventsProvider. WorkspaceMembersModal adds debounced user search, role Select component for inline editing, and member-add flow with success alerts.
Search UI with highlighted text rendering
packages/ui/src/components/highlighted-text.tsx, packages/ui/src/components/note-suggestion-menu.tsx, packages/ui/src/components/note-search-modal.tsx
Introduces HighlightedText component for segment-based highlight rendering. Adds NoteSuggestionMenu dropdown with keyboard/click handling. Updates NoteSearchModal to use HighlightedText for formatted names and snippets. Guards search functions against missing client.
Editor viewer mode and table of contents
packages/ui/src/components/editor-core.tsx, packages/ui/src/components/editor.tsx, packages/ui/src/components/table-of-contents.tsx
EditorCore gains isViewer flag and onEditorReady callback. Editor respects viewer mode via editable={!isViewer}. Editor fetches workspaces, derives isViewer from role, tracks instance. New TableOfContents component extracts headings, tracks scroll progress, and provides in-document navigation.
Graph orphan node filtering
packages/ui/src/components/graph-view.tsx, packages/ui/src/components/graph-settings-dialog.tsx
GraphView tracks showOrphansOnly state and passes to query. GraphSettingsDialog renders "Show Orphans" checkbox when not local, forwarding changes via callback.
Revision modal confirmation and debounced search
packages/ui/src/components/revision-modal.tsx
Adds confirmation dialog for revision application. Introduces debounced search for filtering. Adds theme resolution for read-only editor. Refactors content rendering with loading/error/content states.
Document modified tracking and search caching
packages/ui/src/hooks/use-is-doc-modified.ts, packages/ui/src/hooks/use-editor-state.ts, packages/ui/src/hooks/use-search-cache.ts, packages/ui/src/hooks/use-debounced-value.ts
useIsDocModified refactored to use YDocMetadataMap and returns only { isModified }. Removes in-memory cache from useSearchCache, refactors debounce with doFetch callback. Adds useDebouncedValue hook for generic debouncing.
Workspace sidebar and switcher
packages/ui/src/components/workspace-sidebar.tsx, packages/ui/src/components/workspace-switcher.tsx
Sidebar switches to getAuthClient(), adds project data with URL builders, navigates programmatically. Logout calls signOut() then redirects to /api/auth/logout. Switcher adds alerts to create/update/leave mutations and reorders error handling.
Note menu items with generic typing
packages/ui/src/block-note/menu-states.ts, packages/ui/src/block-note/block-note.ts
getMenuItemsWithState becomes generic, returning T[] instead of always DefaultReactSuggestionItem[]. Casts factory results to preserve caller's type. createBlockNoteSchema accepts optional apiUrl and forwards to reference spec.

Landing page redesign

Layer / File(s) Summary
Landing hero, footer, and navigation
packages/ui/src/components/landing-hero.tsx, packages/ui/src/components/landing-footer.tsx, packages/ui/src/components/landing-navigation-bar.tsx
Hero converted to client component with headline, contact input, two CTA buttons, and feature tiles grid. Footer renamed to LandingFooter with updated branding ("Notopia" / "knowledge graph") and link styling. Navigation simplified to direct render with NAV_ITEMS constant and single flex layout.
Marketing layout refinement
apps/web/app/(marketing)/layout.tsx, apps/web/app/(marketing)/page.tsx
Layout header updated with border-b px-6 py-3 and inner container mx-auto max-w-6xl. Main region gains flex-1. Page simplified to directly return HeroSection without wrapper div.

Infrastructure, database, and build configuration

Layer / File(s) Summary
Docker build and asset placement
apps/web/Dockerfile, .dockerignore
Renames final stage to production. Updates COPY to place public and .next/static under ./apps/web/ instead of ./. Removes .next/cache copying. Adds negation pattern for .next/standalone/node_modules.
SQL query fix and PostgreSQL alignment
internal/note/infra/persistence/pgsqlc/folder.sql, internal/note/infra/persistence/pgsqlc/folder.sql.go
GetParentIDsByFolderID recursive CTE now properly aliases folders table as f in recursive SELECT term.
Tooling and linter configuration
nx.json, apps/web/project.json, packages/ui/oxlint.config.ts
Adds run target with mon configuration. Updates oxlint to add node and react-perf plugins and enforce node/no-process-env: error.
Miscellaneous UI and Hocuspocus updates
apps/document/src/hocuspocus/hocuspocus.ts, packages/ui/src/components/shadcn/popover.tsx, packages/ui/src/components/trashed-file-management.tsx, packages/ui/src/global.d.ts
Hocuspocus server gains onLoadDocument callback for debug logging and guards metadata update on missing connection. Adds shadcn Popover wrapper components. Simplifies trashed-file table styling. Removes global ProcessEnv augmentation from UI package (moved to apps/web).

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • notopia-uit/notopia#232: Introduces workspace SSE event functionality related to the WorkspaceEventsProvider and tree-view event subscription logic.
  • notopia-uit/notopia#223: Overlaps with Hocuspocus/Yjs collaboration changes and document load/change handling.
  • notopia-uit/notopia#177: Modifies the workspace layout and prefetching logic; related to the layout and access-token import rewiring in this PR.
✨ 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 ntgnguyen/doing-5

@codecov
Copy link
Copy Markdown

codecov Bot commented May 30, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR expands the UI editing experience (reference/tag previews, improved search UX, revision history improvements, and a table of contents) while also introducing workspace SSE event plumbing and refactoring environment/config handling for auth + Meilisearch across the Next.js app.

Changes:

  • Added UI features: reference/tag preview dialogs, TOC, richer search highlighting, revision modal UX updates, and workspace member search/add flow.
  • Introduced workspace SSE events context/provider and hooked it into the tree to invalidate queries on updates.
  • Refactored auth client initialization (configurable baseURL) and moved server env typing/config into the web app; updated routing/env usage accordingly.

Reviewed changes

Copilot reviewed 58 out of 61 changed files in this pull request and generated 13 comments.

Show a summary per file
File Description
packages/ui/src/lib/get-access-token-client-side.ts Switches to getAuthClient() access token retrieval.
packages/ui/src/lib/auth-client.ts Refactors auth client into configurable singleton (configureAuthClient/getAuthClient).
packages/ui/src/index.ts Exports the new workspace events context.
packages/ui/src/hooks/use-search-cache.ts Refactors debounce/caching behavior for search.
packages/ui/src/hooks/use-is-doc-modified.ts Updates Yjs metadata map typing/keys and removes setter API.
packages/ui/src/hooks/use-editor-state.ts Adjusts editor state usage to new useIsDocModified return shape.
packages/ui/src/hooks/use-debounced-value.ts Adds a reusable debounced-value hook.
packages/ui/src/global.d.ts Removes UI package env declarations.
packages/ui/src/contexts/workspace-events-context.tsx Adds SSE workspace events provider + subscription API.
packages/ui/src/components/workspace-switcher.tsx Formatting + minor structural adjustments (error UI moved).
packages/ui/src/components/workspace-sidebar.tsx Uses getAuthClient() and updates projects navigation + logout handling.
packages/ui/src/components/workspace-members-modal.tsx Adds server-side user search + “add member” flow with debounced search.
packages/ui/src/components/workspace-content-wrapper.tsx Plumbs Meilisearch host via prop and wraps children in workspace events provider.
packages/ui/src/components/tree-view.tsx Consumes workspace events to invalidate tree query; adjusts navigation handling.
packages/ui/src/components/trashed-file-management.tsx Refactors styling and minor structural cleanup.
packages/ui/src/components/table-of-contents.tsx Adds TOC popover UI driven by BlockNote headings + scroll progress.
packages/ui/src/components/signin-form.tsx Uses getAuthClient() for social sign-in.
packages/ui/src/components/shadcn/popover.tsx Adds shadcn popover primitive wrapper.
packages/ui/src/components/revision-modal.tsx Improves revision modal UX (debounced search, confirm apply, better error handling).
packages/ui/src/components/note-title.tsx Invalidates workspace tree after note rename.
packages/ui/src/components/note-suggestion-menu.tsx Adds custom suggestion rendering for note mentions.
packages/ui/src/components/note-search-modal.tsx Adds highlighted matches and safer search behavior when Meilisearch client is missing.
packages/ui/src/components/landing-navigation-bar.tsx Updates marketing nav layout and link/button structure.
packages/ui/src/components/landing-hero.tsx Replaces hero section with new marketing design.
packages/ui/src/components/landing-footer.tsx Renames and redesigns marketing footer.
packages/ui/src/components/highlighted-text.tsx Adds shared component to render Meilisearch-highlighted strings.
packages/ui/src/components/graph-view.tsx Adds orphan filtering support to graph query + settings wiring.
packages/ui/src/components/graph-settings-dialog.tsx Adds “Show Orphans” toggle (global graph only).
packages/ui/src/components/editor.tsx Adds viewer-mode support, TOC rendering, and workspace role lookup.
packages/ui/src/components/editor-core.tsx Adds viewer mode, note suggestion menu component, editor-ready callback.
packages/ui/src/block-note/tag.tsx Adds tag click preview dialog with Meilisearch lookup + navigation.
packages/ui/src/block-note/reference.tsx Adds reference click preview dialog with embedded read-only editor.
packages/ui/src/block-note/menu.ts Adds Meilisearch highlight parsing and richer search result fields/snippets.
packages/ui/src/block-note/menu-states.ts Makes menu state helper generic for richer suggestion items.
packages/ui/src/block-note/block-note.ts Extends schema creation to accept API URL for reference previews.
packages/ui/oxlint.config.ts Enables node plugin + disallows process.env usage in UI package.
nx.json Adds run.configurations.mon.
internal/note/infra/persistence/pgsqlc/folder.sql.go SQLC output updated: aliases folders AS f in recursive query.
internal/note/infra/persistence/pgsqlc/folder.sql Source SQL updated to match aliasing change.
apps/web/tsconfig.json Adds @lib/* path alias and includes lib/**/*.ts.
apps/web/proxy.ts Switches auth import to local ./lib/auth.
apps/web/project.json Adds start.configurations.mon.
apps/web/lib/get-access-token.ts Switches auth import to local ./auth.
apps/web/lib/auth.ts Switches access token import to local helper.
apps/web/global.d.ts Adds NodeJS ProcessEnv typing for web app.
apps/web/Dockerfile Adjusts standalone/static/public copy layout for production image.
apps/web/app/layout.tsx Wraps app in EnvInit to configure auth client baseURL.
apps/web/app/env-init.tsx Adds client-side auth client configuration initializer.
apps/web/app/api/auth/logout/route.ts Adds Authentik end-session redirect endpoint.
apps/web/app/api/auth/[...all]/route.ts Updates auth import alias.
apps/web/app/(workspace)/workspace/page.tsx Updates access token helper import (@lib).
apps/web/app/(workspace)/workspace/[workspaceId]/page.tsx Updates access token helper import (@lib).
apps/web/app/(workspace)/workspace/[workspaceId]/note/[noteId]/page.tsx Uses API_URL for WS URL.
apps/web/app/(workspace)/workspace/[workspaceId]/layout.tsx Passes Meilisearch host into UI wrapper.
apps/web/app/(workspace)/workspace/[workspaceId]/graph/page.tsx Updates access token helper import (@lib).
apps/web/app/(marketing)/page.tsx Simplifies marketing page wrapper.
apps/web/app/(marketing)/layout.tsx Updates marketing layout spacing/container usage.
apps/web/.env Adds server-side API_URL/MEILISEARCH_HOST and keeps other env values.
apps/document/src/hocuspocus/hocuspocus.ts Adds onLoadDocument debug logging for document metadata.
apps/document/.env Fixes NODE_ENV key name.
.dockerignore Allows .next/standalone/node_modules to be included.
Files not reviewed (1)
  • internal/note/infra/persistence/pgsqlc/folder.sql.go: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 49 to 55
return new Promise((resolve) => {
timeoutRef.current = setTimeout(async () => {
try {
const result = await searchFn(query);
setIsLoading(false);
setError(null);
resolve({ data: result, isLoading: false, error: null });
} catch (err) {
const error = err instanceof Error ? err : new Error('Unknown error occurred');
setError(error);
setIsLoading(false);
resolve({ data: undefined as T, isLoading: false, error });
}
if (inflightQueryRef.current !== query) return;

const result = await doFetch(query);
resolve(result);
}, debounceMs);
Comment on lines +16 to 20
const observer = (event: Y.YMapEvent<YDocMetadata>) => {
if (event.keysChanged.has('metadata')) {
setIsModified(metadata?.modified === true);
}
};
if (isError) {
return (
<div className="flex h-[400px] items-center justify-center">
<div className="flex h-100 items-center justify-center">

{!isCollapsed && (
<ScrollArea className="h-[400px] pr-4">
<ScrollArea className="h-100 pr-4">
className="flex max-h-[80vh] max-w-4xl flex-col gap-0 overflow-hidden p-0"
showCloseButton={true}
>
<div className="min-h-75 flex-1 overflow-auto">
Comment on lines +21 to +25
if (!meilisearchHost) {
console.warn(
'MEILISEARCH_HOST is not set. Falling back to http://localhost:7700'
);
}
Comment on lines +3 to +12
export function GET() {
const endSessionUrl = process.env.AUTHENTIK_CLIENT_DISCOVERY_URL?.replace(
'/.well-known/openid-configuration',
'/end-session/'
);

const redirectUri = encodeURIComponent(`${process.env.BETTER_AUTH_URL}/signin`);

return NextResponse.redirect(`${endSessionUrl}?post_logout_redirect_uri=${redirectUri}`);
}
Comment thread apps/web/.env
Comment on lines 6 to 10
NEXT_PUBLIC_API_URL="api.notopia.localhost"
API_URL="api.notopia.localhost"
AUTHENTIK_CLIENT_ID="app-web"
AUTHENTIK_CLIENT_SECRET="notopiauit"
AUTHENTIK_CLIENT_DISCOVERY_URL="http://authentik.notopia.localhost/application/o/app-web/.well-known/openid-configuration"
Comment on lines +96 to +114
<PopoverTrigger asChild>
<div
className="flex cursor-default flex-col items-center gap-4 px-1.5 py-3"
onMouseEnter={() => setOpen(true)}
onMouseLeave={() => setOpen(false)}
>
{Array.from({ length: SEGMENTS }).map((_, i) => (
<div
key={i}
className={cn(
'h-1 w-5 rounded-full transition-all duration-200',
i < filledSegments
? 'bg-foreground/40 scale-115'
: 'bg-foreground/10 scale-100'
)}
/>
))}
</div>
</PopoverTrigger>
Comment on lines +30 to +36
onLoadDocument: async (data) => {
this.logger.debug(
{ documentId: data.documentName, documentMetadata: data.document.getMap('metadata') },
'Document loaded'
);
await Promise.resolve();
},
@KevinNitroG KevinNitroG removed their request for review May 30, 2026 14:28
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 28

Caution

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

⚠️ Outside diff range comments (5)
packages/ui/src/components/note-title.tsx (1)

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

Always invalidate the note query after rename, regardless of workspaceId.

Current logic skips getNoteOptions invalidation when workspaceId is undefined, which can leave stale note title data in non-workspace contexts.

Suggested fix
   const { mutate: renameNote, isPending: isRenamingNote } = useRenameNoteMutation({
     onSuccess: () => {
-      if (workspaceId) {
-        queryClient.invalidateQueries({
-          queryKey: getNoteOptions({ path: { noteId } }).queryKey,
-        });
+      queryClient.invalidateQueries({
+        queryKey: getNoteOptions({ path: { noteId } }).queryKey,
+      });
+      if (workspaceId) {
         queryClient.invalidateQueries({
           queryKey: getWorkspaceTreeOptions({ path: { workspaceId } }).queryKey,
         });
       }
     },
   });
🤖 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 `@packages/ui/src/components/note-title.tsx` around lines 30 - 38, The
onSuccess handler currently only invalidates getNoteOptions when workspaceId is
truthy, leaving note title data stale in non-workspace contexts; change the
logic in the onSuccess callback so queryClient.invalidateQueries({ queryKey:
getNoteOptions({ path: { noteId } }).queryKey }) is always called, and keep the
existing conditional around queryClient.invalidateQueries({ queryKey:
getWorkspaceTreeOptions({ path: { workspaceId } }).queryKey }) so the workspace
tree is only invalidated when workspaceId exists.
apps/web/Dockerfile (1)

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

Drop root privileges in the runtime image.

This stage still runs the app as root. That unnecessarily widens the blast radius of any server-side compromise; add a dedicated runtime user and switch to it before the entrypoint.

Proposed fix
 FROM node:25.6.1-alpine3.23 AS production
 ENV NODE_ENV=production
 ENV NEXT_TELEMETRY_DISABLED=1
 WORKDIR /app
-COPY apps/web/.next/standalone ./
-COPY apps/web/public ./apps/web/public
-COPY apps/web/.next/static ./apps/web/.next/static
+RUN addgroup -S nodejs && adduser -S nextjs -G nodejs
+COPY --chown=nextjs:nodejs apps/web/.next/standalone ./
+COPY --chown=nextjs:nodejs apps/web/public ./apps/web/public
+COPY --chown=nextjs:nodejs apps/web/.next/static ./apps/web/.next/static
+USER nextjs
 ENV PORT=3000
 ENV HOSTNAME="0.0.0.0"
 EXPOSE 3000
 ENTRYPOINT ["node", "apps/web/server.js"]
🤖 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 `@apps/web/Dockerfile` around lines 1 - 11, The runtime image currently runs as
root; create a non-root user and switch to it before starting the app by adding
a dedicated runtime user (e.g., "appuser") and group, change ownership of the
application files under WORKDIR to that user, and set USER to that username so
ENTRYPOINT ["node","apps/web/server.js"] runs unprivileged; update the
Dockerfile around WORKDIR/COPY steps and before ENTRYPOINT to add the
user/group, chown the copied paths (./apps/web/* and ./.next/*) to that user,
and set USER appuser.
packages/ui/src/components/workspace-content-wrapper.tsx (1)

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

Do not fall back to localhost in the browser.

When meilisearchHost is missing, this silently points users at their own machine and turns a configuration error into opaque network failures. Disable search features or render an explicit misconfiguration state instead of defaulting to http://localhost:7700.

Suggested fix
-  if (!meilisearchHost) {
-    console.warn(
-      'MEILISEARCH_HOST is not set. Falling back to http://localhost:7700'
-    );
-  }
+  if (!meilisearchHost) {
+    console.warn('MEILISEARCH_HOST is not set. Search features will be disabled.');
+  }
@@
-    <MeilisearchProvider
-      host={meilisearchHost || 'http://localhost:7700'}
-      apiKey={tokenData?.token}
-    >
-      <WorkspaceEventsProvider workspaceId={workspaceId}>
-        {children}
-      </WorkspaceEventsProvider>
-      <NoteSearchModal workspaceId={workspaceId} />
-    </MeilisearchProvider>
+    <WorkspaceEventsProvider workspaceId={workspaceId}>
+      {meilisearchHost ? (
+        <MeilisearchProvider host={meilisearchHost} apiKey={tokenData?.token}>
+          {children}
+          <NoteSearchModal workspaceId={workspaceId} />
+        </MeilisearchProvider>
+      ) : (
+        children
+      )}
+    </WorkspaceEventsProvider>
🤖 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 `@packages/ui/src/components/workspace-content-wrapper.tsx` around lines 21 -
37, The current code silently falls back to 'http://localhost:7700' when
meilisearchHost is missing; update the component (references: meilisearchHost,
MeilisearchProvider, useQuery/getWorkspaceSearchTokenOptions, tokenData) to NOT
default to localhost — instead, detect when meilisearchHost is falsy and render
a clear misconfiguration state or disable search UI (e.g., do not render
MeilisearchProvider and show a warning/error component), and ensure apiKey/token
handling still guards against undefined tokenData; remove the fallback value and
replace with a conditional render that surfaces the configuration error to the
user or disables search features.
packages/ui/src/components/editor.tsx (1)

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

Default unknown workspace roles to read-only.

Whilst getMyWorkspacesOptions() is still loading, currentWorkspace is undefined, so isViewer becomes false and viewer members get an editable editor on first render. Treat an unknown membership/role as read-only, or delay rendering EditorCore until the query resolves.

Suggested fix
   const { data: allWorkspaceData } = useQuery({
     ...getMyWorkspacesOptions({}),
   });
-  const currentWorkspace = allWorkspaceData?.find((ws) => ws.workspace.id === workspaceId);
-  const isViewer = currentWorkspace?.role === 'viewer';
+  const currentWorkspace = workspaceId
+    ? allWorkspaceData?.find((ws) => ws.workspace.id === workspaceId)
+    : undefined;
+  const isViewer =
+    workspaceId != null &&
+    (!currentWorkspace ||
+      currentWorkspace.role === 'viewer');
🤖 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 `@packages/ui/src/components/editor.tsx` around lines 40 - 68, When workspace
membership is unknown during the query, default to read-only to avoid briefly
showing an editable editor: change the isViewer logic so unknown membership
yields read-only (e.g., const isViewer = allWorkspaceData ?
currentWorkspace?.role === 'viewer' : true) and additionally guard rendering
EditorCore until the query resolves (only render EditorCore when
allWorkspaceData is defined) so the editor isn't mounted with incorrect
permissions; update references in this file (getMyWorkspacesOptions,
currentWorkspace, isViewer, EditorCore, editorRef) accordingly.
packages/ui/src/components/editor-core.tsx (1)

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

Fix forwardRef ref handling to support callback refs

In packages/ui/src/components/editor-core.tsx (lines 156-168), the effect casts ref to React.MutableRefObject and only sets .current; if a consumer passes a callback ref, it will never be called (and cleanup won’t clear it via the callback), breaking the forwardRef ref contract.

Suggested fix
-import { forwardRef, useMemo, useCallback, useEffect } from 'react';
+import { forwardRef, useMemo, useCallback, useEffect, useImperativeHandle } from 'react';
@@
-  useEffect(() =&gt; {
-    if (ref) {
-      (ref as React.MutableRefObject&lt;any&gt;).current = editor;
-    }
-    if (onEditorReady) {
-      onEditorReady(editor);
-    }
-    return () =&gt; {
-      if (ref) {
-        (ref as React.MutableRefObject&lt;any&gt;).current = null;
-      }
-    };
-  }, [editor, ref, onEditorReady]);
+  useImperativeHandle(ref, () =&gt; editor, [editor]);
+
+  useEffect(() =&gt; {
+    onEditorReady?.(editor);
+  }, [editor, onEditorReady]);
🤖 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 `@packages/ui/src/components/editor-core.tsx` around lines 156 - 168, The
effect currently assumes ref is a MutableRefObject and only sets ref.current;
change it to handle both object refs and callback refs: inside the useEffect
that references editor, ref, and onEditorReady, detect if typeof ref ===
'function' and call ref(editor) (and on cleanup call ref(null)), otherwise cast
to React.MutableRefObject and set .current = editor (and .current = null in
cleanup); keep calling onEditorReady(editor) as before. This preserves
forwardRef callback semantics and proper cleanup.
🤖 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 @.dockerignore:
- Around line 2-3: The current .dockerignore negation only matches the directory
path and not its contents, so the standalone bundle's dependencies remain
excluded; update the negation to re-include the subtree by changing
"!**/.next/standalone/node_modules" to include contents (for example
"!**/.next/standalone/node_modules/**" and optionally also
"!**/.next/standalone/node_modules") while keeping the general exclusion
"**/node_modules".

In `@apps/document/src/hocuspocus/hocuspocus.ts`:
- Around line 30-33: The onLoadDocument handler is currently logging the full
Yjs metadata map (data.document.getMap('metadata')), which may expose sensitive
or oversized data; update onLoadDocument to extract and log only bounded
non-sensitive fields (e.g. const meta = data.document.getMap('metadata'); const
modified = meta.get('modified'); const keyCount = meta.size or
meta.keys().length) and pass those values to this.logger.debug along with
documentId instead of the entire map; locate the onLoadDocument callback and
replace the logger.debug payload accordingly so only safe, small fields are
logged.

In `@apps/web/app/`(workspace)/workspace/[workspaceId]/note/[noteId]/page.tsx:
- Line 15: The WebSocket URL construction using
url={`ws://${process.env.API_URL}/document/ws/document`} is protocol‑fragile;
change it to build the ws/wss scheme dynamically from process.env.API_URL (or
parse it with the URL API) so if API_URL already contains a scheme you convert
http->ws and https->wss (or strip any existing scheme and prepend the correct
ws/wss), and ensure the final value passed to the url prop is a valid ws:// or
wss:// URL.

In `@apps/web/app/api/auth/logout/route.ts`:
- Around line 3-11: The GET handler builds endSessionUrl and redirectUri from
envs without validation; update the GET function to validate that
process.env.AUTHENTIK_CLIENT_DISCOVERY_URL and process.env.BETTER_AUTH_URL are
present and that AUTHENTIK_CLIENT_DISCOVERY_URL contains
'/.well-known/openid-configuration' before calling replace; if validation fails,
return a controlled error response (e.g., NextResponse.json or
NextResponse.redirect to a safe error page) with an appropriate 4xx/5xx status
instead of redirecting to an invalid URL; ensure you reference and check the
endSessionUrl and redirectUri construction paths so the logic short-circuits on
missing/invalid envs.

In `@apps/web/app/env-init.tsx`:
- Around line 14-22: EnvInit currently calls configureAuthClient(betterAuthUrl
|| 'http://localhost:3000') during render guarded by a module-level configured
flag; move that side-effect into a React useEffect inside the EnvInit component
(remove the module-level configured mutable variable) so configuration runs only
after mount and is safe under StrictMode. Inside the effect, preserve the
existing warning when betterAuthUrl is falsy and call
configureAuthClient(betterAuthUrl || 'http://localhost:3000'); use an empty
dependency array (or a stable ref) to ensure it only runs once.

In `@apps/web/app/layout.tsx`:
- Around line 46-48: The layout currently passes a fallback
"http://localhost:3000" to EnvInit which can hide misconfigurations; instead,
read process.env.BETTER_AUTH_URL and fail fast if it's undefined or empty, then
pass that value into EnvInit. Update the code around EnvInit so it does not
provide a localhost default — validate BETTER_AUTH_URL (e.g., throw or assert
with a clear message mentioning BETTER_AUTH_URL) before rendering and then
supply the validated string to the EnvInit betterAuthUrl prop.

In `@packages/ui/src/block-note/menu.ts`:
- Around line 123-125: The Meilisearch filter is built by interpolating raw tag
text into `tags = "${tag}"` in the index.search call, which breaks or allows
injection when tag contains `"` or `\`; fix it by escaping backslashes and
double quotes in the tag before interpolation (or otherwise using a safe filter
construction API) so the filter string is always valid; update the code around
the `index.search('', { filter: [\`tags = "${tag}"\`], limit: 50 })` call to
sanitize `tag` (escape `\` and `"` characters) or use a safe filter-building
helper, ensuring the search uses the escaped value.

In `@packages/ui/src/block-note/reference.tsx`:
- Around line 88-95: The anchor's fallback href uses `/note/${noteId}` but the
real route is nested under the workspace path, so update the element that
renders the preview link: either construct the correct absolute route (e.g.,
`/workspace/${workspaceId}/note/${noteId}`) and use that as the href, or replace
the <a> with a semantic <button> (preserving className, data-notopia-ref and the
onClick that calls setShowPreview) to avoid misleading open-in-new-tab/copy-link
behavior; locate the anchor using the attributes noteId, href, onClick and
setShowPreview and make the change accordingly so keyboard/accessibility
(role/aria) is preserved.
- Around line 109-115: The preview socket URL construction in
createBlockNoteReferenceSpec currently hardcodes "ws://", which breaks when
apiUrl includes a scheme or when the page is served over HTTPS; update the
previewWsUrl logic to derive the WebSocket scheme and host from apiUrl (or from
window.location if apiUrl is relative/missing): if apiUrl starts with "http://"
or "https://", map "http"->"ws" and "https"->"wss" and replace the scheme before
appending "/document/ws/document"; if apiUrl has no scheme, use
window.location.protocol to choose "ws" vs "wss" and build the full URL
accordingly so previewWsUrl always uses the correct ws/wss scheme.

In `@packages/ui/src/components/graph-settings-dialog.tsx`:
- Line 335: The onCheckedChange handlers in graph-settings-dialog.tsx (e.g., the
one calling onOrphansChange) unsafely cast Radix CheckedState to boolean; change
each handler to pass a strict boolean by using a strict equality check (e.g.,
checked === true) instead of casting, update all similar checkbox handlers in
this file (those that call onOrphansChange, and other onXChange callbacks) to
use checked === true so 'indeterminate' is handled safely and only true becomes
true.

In `@packages/ui/src/components/landing-footer.tsx`:
- Around line 17-21: Replace the non-interactive <span> elements that render
"Blog", "Docs", and "Source" in the landing-footer component with real
interactive link elements so they are keyboard-accessible and navigable: locate
the three spans (text nodes "Blog", "Docs", "Source") and change them to <a> (or
your framework's Link) elements with appropriate hrefs (e.g., blog URL, docs
route, repository URL), keep the existing className ("hover:text-foreground
cursor-pointer transition-colors") for styling, and ensure external links
include target="_blank" rel="noopener noreferrer" where applicable; leave the
Separator component usage unchanged.

In `@packages/ui/src/components/landing-hero.tsx`:
- Around line 34-37: The contact input currently uses only placeholder text
("Contact us") which is not a reliable accessible name; update the Input element
(the Input component instance with placeholder "Contact us") to provide an
explicit accessible name by adding an aria-label (e.g., aria-label="Contact us")
or by associating it with a visible or visually-hidden <label> (generate an id
on the Input and reference it from the label) so assistive technologies get a
proper name.

In `@packages/ui/src/components/landing-navigation-bar.tsx`:
- Around line 28-33: The menu items are rendered as interactive controls but
lack navigation targets; update NAV_ITEMS to be an array of objects like {label,
href} (or add hrefs where NAV_ITEMS is defined), then replace the current
NavigationMenuTrigger usage inside the NAV_ITEMS.map with a link-capable element
(e.g., NavigationMenuLink or your app's Link component) that uses the item's
href so clicking the NavigationMenuItem navigates; also wire the GitHub CTA (the
button rendered at the other block) to a proper anchor or Link with href,
target="_blank" and rel="noopener noreferrer" so it actually opens the GitHub
destination.

In `@packages/ui/src/components/revision-modal.tsx`:
- Around line 133-139: handleOpenChange currently resets setSelectedRevisionId
and setSearchQuery when the modal closes but doesn't clear the confirmApply
flag; update the callback (handleOpenChange) so that when newOpen is false you
also call setConfirmApply(false) to reset the confirmation dialog state
(alongside setSelectedRevisionId(null) and setSearchQuery('')) so confirmApply
cannot remain stale after the main dialog closes.
- Around line 44-49: resolvedTheme from useTheme() can be undefined and the
current type assertion hides that risk; in revision-modal.tsx compute a safe
theme value (e.g., const theme = resolvedTheme ?? 'light' or normalize with
(resolvedTheme === 'dark' ? 'dark' : 'light')) and pass that to BlockNoteView
instead of casting resolvedTheme, updating the code where useTheme() is called
and where BlockNoteView is rendered to use the new theme variable.

In `@packages/ui/src/components/shadcn/popover.tsx`:
- Around line 58-64: PopoverTitle is typed with React.ComponentProps<"h2"> but
returns a div, breaking semantics and accessibility; change the JSX element
returned by the PopoverTitle function from <div> to an <h2> while keeping
data-slot="popover-title", the cn("font-medium", className) className logic and
spreading {...props} so the component's props and styling remain unchanged.

In `@packages/ui/src/components/table-of-contents.tsx`:
- Around line 95-122: The TOC popover is currently opened only via hover
handlers on a non-focusable div, preventing keyboard/touch access; replace the
PopoverTrigger child div with a focusable shadcn Button (use the existing
Popover, PopoverTrigger and PopoverContent) and remove the
onMouseEnter/onMouseLeave handlers from both the trigger and PopoverContent so
Radix/Popover handles click and focus for keyboard/touch users; keep the visual
rendering using SEGMENTS and filledSegments and retain setOpen only if you still
need controlled open state, otherwise rely on Radix's default open behavior.

In `@packages/ui/src/components/trashed-file-management.tsx`:
- Around line 342-349: The two DropdownMenuItem entries are just UI-only; wire
them to the restore and permanent-delete mutations by adding onClick/onSelect
handlers that call the existing mutation functions (e.g.,
restoreFileMutation.mutate or deleteFileMutation.mutate) with the current file
identifier (e.g., file.id or trashedItem.id) and handle optimistic UI/update on
success; update the DropdownMenuItem for the RotateCcw item to call the restore
handler and the Trash2 item to call the delete-permanent handler, and ensure any
required imports (restore/delete mutation hooks) and loading/disabled states are
applied so the actions actually execute.

In `@packages/ui/src/components/workspace-members-modal.tsx`:
- Around line 107-118: The current useQuery call that sets const { data:
searchUserData = [], isFetching: isSearchingUsers } = useQuery({...}) masks
network/errors by defaulting data to []—capture and expose the query error state
by adding isError and error to the destructure (e.g. const { data:
searchUserData = [], isFetching: isSearchingUsers, isError:
isSearchingUsersError, error: searchUsersError } = useQuery({...})) for the
useQuery invoked with searchUsersOptions and select: mapDtoToSearchUserMembers,
then update the UI rendering where searchUserData/isSearchingUsers are used
(including the similar block around lines 237-249) to show an explicit
error/failure state or alert when isSearchingUsersError is true using
searchUsersError instead of showing the "No users found" empty state.

In `@packages/ui/src/components/workspace-sidebar.tsx`:
- Around line 440-444: The onClick handler's await getAuthClient().signOut() can
reject and currently prevents navigation to the provider logout, so wrap the
sign-out call in a try/catch or use try { await getAuthClient().signOut(); }
catch (err) { /* optional log via console or process logger */ } finally {
window.location.href = '/api/auth/logout'; } to ensure the DropdownMenuItem
click always navigates to '/api/auth/logout' even if getAuthClient().signOut()
fails.

In `@packages/ui/src/components/workspace-switcher.tsx`:
- Around line 324-343: The role selectors (Select using
value={editForm.userRole}, setEditForm and RoleSelectItems) are only updating UI
state and not persisted because the slug update and workspace creation mutations
do not include userRole in their payloads; either remove these Select controls
from the workspace-switcher UI or wire them to a real role-management mutation
by (a) adding userRole to the payload of the workspace creation and slug update
flows (ensure the handlers that call those mutations—where the createWorkspace
and updateWorkspaceSlug functions are invoked—read editForm.userRole and pass it
through), or (b) call a dedicated role-assignment API/mutation after save (e.g.,
assignRoleToUser(workspaceId, userId, editForm.userRole)), and ensure the
SelectTrigger onClick still stops propagation and the SelectValue reflects the
persisted value.
- Around line 127-131: The alert currently exposes raw backend error details by
interpolating error.message into showAlert; change the user-facing message to a
generic, non-sensitive string (e.g., "There was an error creating your
workspace. Please try again.") and remove interpolation of error.message from
the message passed to showAlert; instead send the full error to your
telemetry/logging helper (e.g., reportError, logError, or the existing telemetry
client) so detailed diagnostics are recorded. Apply the same fix for the other
occurrence around lines 185-189, and ensure you reference the same showAlert
call sites and pass the error object to your telemetry function rather than to
the visible alert.

In `@packages/ui/src/contexts/workspace-events-context.tsx`:
- Around line 38-40: The fan-out in onSseEvent uses
handlersRef.current.forEach((handler) => handler(raw.data)) so a throwing
handler aborts delivery; change this to call each handler inside its own
try/catch (e.g., iterate handlersRef.current and for each handler invoke
handler(raw.data) inside try { ... } catch (err) { /* log error */ }) so one
subscriber failure doesn't stop others, and log the error with context including
the handler identity or event data.
- Around line 33-55: The connect logic in connect/getWorkspaceEvents currently
stops on first error or when the stream closes; wrap the connection lifecycle in
a retry loop with exponential backoff so it reconnects until the AbortController
is signaled. Specifically, modify the connect function used in the
workspace-events effect to: (1) loop while !abortController.signal.aborted, (2)
call getWorkspaceEvents(...) and await/consume its stream inside a try block,
(3) on success reset the backoff, (4) on any thrown error or when the
for-await-of completes treat it as a disconnect and wait an increasing delay
(with a max cap) before retrying, and (5) ensure you still respect
onSseEvent/onSseError handlersRef and abortController.signal to break the loop
promptly; update functions named connect and the effect that calls it
accordingly.

In `@packages/ui/src/hooks/use-is-doc-modified.ts`:
- Line 27: The effect in the useIsDocModified hook currently includes clientId
in its dependency array though clientId is not referenced inside the effect
body; update the hook by removing clientId from the useEffect dependency array
(leaving [ydoc]) to avoid unnecessary re-runs, or if clientId is truly unused
anywhere in the hook, remove the clientId parameter from the useIsDocModified
signature and all callers instead; locate references to useIsDocModified and the
useEffect that ends with "}, [ydoc, clientId]);" and apply one of these two
fixes consistently.
- Around line 16-20: The observer callback in use-is-doc-modified captures a
stale metadata variable, so update the observer to read the current metadata
from the Y map at runtime (e.g., use event.target.get('metadata') or fetch from
the Y doc/map inside the observer) instead of referencing the closed-over
metadata; then call setIsModified(currentMetadata?.modified === true) so
isModified reflects real-time changes (ensure the observer signature remains
Y.YMapEvent<YDocMetadata> and is registered/unregistered the same way).

In `@packages/ui/src/hooks/use-search-cache.ts`:
- Around line 49-56: The returned Promise in the debounce block (inside
use-search-cache's search logic) can remain unresolved if
inflightQueryRef.current !== query; modify that early-return to settle the
Promise by either resolving with a defined "stale" result (e.g., null or an
empty SearchResult) or rejecting with a specific CancelledError so callers can
distinguish cancellation from real failures; ensure you also clear
timeoutRef.current and do any necessary cleanup before resolving/rejecting, and
update callers of search() to handle the chosen sentinel or error type.

In `@packages/ui/src/lib/auth-client.ts`:
- Around line 7-20: getAuthClient currently silently defaults to a localhost
base URL when configureAuthClient hasn't been called; change getAuthClient to
fail fast by checking _baseURL (and treating empty string as unset) and throw a
clear error instructing callers to call configureAuthClient(baseURL) first; keep
the existing createAuthClient usage and _client caching behavior (resetting
_client in configureAuthClient remains fine) so the only change is adding a
guard at the start of getAuthClient that throws a descriptive Error if _baseURL
is falsy.

---

Outside diff comments:
In `@apps/web/Dockerfile`:
- Around line 1-11: The runtime image currently runs as root; create a non-root
user and switch to it before starting the app by adding a dedicated runtime user
(e.g., "appuser") and group, change ownership of the application files under
WORKDIR to that user, and set USER to that username so ENTRYPOINT
["node","apps/web/server.js"] runs unprivileged; update the Dockerfile around
WORKDIR/COPY steps and before ENTRYPOINT to add the user/group, chown the copied
paths (./apps/web/* and ./.next/*) to that user, and set USER appuser.

In `@packages/ui/src/components/editor-core.tsx`:
- Around line 156-168: The effect currently assumes ref is a MutableRefObject
and only sets ref.current; change it to handle both object refs and callback
refs: inside the useEffect that references editor, ref, and onEditorReady,
detect if typeof ref === 'function' and call ref(editor) (and on cleanup call
ref(null)), otherwise cast to React.MutableRefObject and set .current = editor
(and .current = null in cleanup); keep calling onEditorReady(editor) as before.
This preserves forwardRef callback semantics and proper cleanup.

In `@packages/ui/src/components/editor.tsx`:
- Around line 40-68: When workspace membership is unknown during the query,
default to read-only to avoid briefly showing an editable editor: change the
isViewer logic so unknown membership yields read-only (e.g., const isViewer =
allWorkspaceData ? currentWorkspace?.role === 'viewer' : true) and additionally
guard rendering EditorCore until the query resolves (only render EditorCore when
allWorkspaceData is defined) so the editor isn't mounted with incorrect
permissions; update references in this file (getMyWorkspacesOptions,
currentWorkspace, isViewer, EditorCore, editorRef) accordingly.

In `@packages/ui/src/components/note-title.tsx`:
- Around line 30-38: The onSuccess handler currently only invalidates
getNoteOptions when workspaceId is truthy, leaving note title data stale in
non-workspace contexts; change the logic in the onSuccess callback so
queryClient.invalidateQueries({ queryKey: getNoteOptions({ path: { noteId }
}).queryKey }) is always called, and keep the existing conditional around
queryClient.invalidateQueries({ queryKey: getWorkspaceTreeOptions({ path: {
workspaceId } }).queryKey }) so the workspace tree is only invalidated when
workspaceId exists.

In `@packages/ui/src/components/workspace-content-wrapper.tsx`:
- Around line 21-37: The current code silently falls back to
'http://localhost:7700' when meilisearchHost is missing; update the component
(references: meilisearchHost, MeilisearchProvider,
useQuery/getWorkspaceSearchTokenOptions, tokenData) to NOT default to localhost
— instead, detect when meilisearchHost is falsy and render a clear
misconfiguration state or disable search UI (e.g., do not render
MeilisearchProvider and show a warning/error component), and ensure apiKey/token
handling still guards against undefined tokenData; remove the fallback value and
replace with a conditional render that surfaces the configuration error to the
user or disables search features.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 7a736007-5def-4aec-a026-14be53563316

📥 Commits

Reviewing files that changed from the base of the PR and between fb0efc7 and 8a9e368.

📒 Files selected for processing (61)
  • .dockerignore
  • apps/document/.env
  • apps/document/src/hocuspocus/hocuspocus.ts
  • apps/web/.env
  • apps/web/Dockerfile
  • apps/web/app/(marketing)/layout.tsx
  • apps/web/app/(marketing)/page.tsx
  • apps/web/app/(workspace)/workspace/[workspaceId]/graph/page.tsx
  • apps/web/app/(workspace)/workspace/[workspaceId]/layout.tsx
  • apps/web/app/(workspace)/workspace/[workspaceId]/note/[noteId]/page.tsx
  • apps/web/app/(workspace)/workspace/[workspaceId]/page.tsx
  • apps/web/app/(workspace)/workspace/page.tsx
  • apps/web/app/api/auth/[...all]/route.ts
  • apps/web/app/api/auth/logout/route.ts
  • apps/web/app/env-init.tsx
  • apps/web/app/layout.tsx
  • apps/web/global.d.ts
  • apps/web/lib/auth.ts
  • apps/web/lib/get-access-token.ts
  • apps/web/project.json
  • apps/web/proxy.ts
  • apps/web/tsconfig.json
  • internal/note/infra/persistence/pgsqlc/folder.sql
  • internal/note/infra/persistence/pgsqlc/folder.sql.go
  • nx.json
  • packages/ui/oxlint.config.ts
  • packages/ui/src/block-note/block-note.ts
  • packages/ui/src/block-note/menu-states.ts
  • packages/ui/src/block-note/menu.ts
  • packages/ui/src/block-note/reference.tsx
  • packages/ui/src/block-note/tag.tsx
  • packages/ui/src/components/editor-core.tsx
  • packages/ui/src/components/editor.tsx
  • packages/ui/src/components/graph-settings-dialog.tsx
  • packages/ui/src/components/graph-view.tsx
  • packages/ui/src/components/highlighted-text.tsx
  • packages/ui/src/components/landing-footer.tsx
  • packages/ui/src/components/landing-hero.tsx
  • packages/ui/src/components/landing-navigation-bar.tsx
  • packages/ui/src/components/note-search-modal.tsx
  • packages/ui/src/components/note-suggestion-menu.tsx
  • packages/ui/src/components/note-title.tsx
  • packages/ui/src/components/revision-modal.tsx
  • packages/ui/src/components/shadcn/popover.tsx
  • packages/ui/src/components/signin-form.tsx
  • packages/ui/src/components/table-of-contents.tsx
  • packages/ui/src/components/trashed-file-management.tsx
  • packages/ui/src/components/tree-view.tsx
  • packages/ui/src/components/workspace-content-wrapper.tsx
  • packages/ui/src/components/workspace-members-modal.tsx
  • packages/ui/src/components/workspace-sidebar.tsx
  • packages/ui/src/components/workspace-switcher.tsx
  • packages/ui/src/contexts/workspace-events-context.tsx
  • packages/ui/src/global.d.ts
  • packages/ui/src/hooks/use-debounced-value.ts
  • packages/ui/src/hooks/use-editor-state.ts
  • packages/ui/src/hooks/use-is-doc-modified.ts
  • packages/ui/src/hooks/use-search-cache.ts
  • packages/ui/src/index.ts
  • packages/ui/src/lib/auth-client.ts
  • packages/ui/src/lib/get-access-token-client-side.ts
💤 Files with no reviewable changes (1)
  • packages/ui/src/global.d.ts

Comment thread .dockerignore
Comment on lines 2 to +3
**/node_modules
!**/.next/standalone/node_modules
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

In Docker .dockerignoresyntax, if/node_modulesis ignored and!/.next/standalone/node_modulesis added, are files under that directory re-included automatically, or do you also need!/.next/standalone/node_modules/?

💡 Result:

You need the second rule. In .dockerignore, a negation rule like !/.next/standalone/node_modules only re-includes what it explicitly matches (that exact directory path). It does not automatically re-include the directory’s contents; those subpaths can still be excluded by the earlier /node_modules pattern. To re-include the contents, add:!/.next/standalone/node_modules/ Why: .dockerignore treats ! lines as exceptions to prior exclusion patterns, and the Docker docs/behavior follow “last matching line wins” semantics for inclusion/exclusion per path [1]. Additionally, it’s not possible to re-include a file if any parent directory of that file is excluded [2], so you must ensure the matching rule covers the subpaths you want included. So: !/.next/standalone/node_modules is not sufficient by itself; include / as well.

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail
ls -la .dockerignore
nl -ba .dockerignore | sed -n '1,120p'

Repository: notopia-uit/notopia

Length of output: 186


🏁 Script executed:

#!/bin/bash
set -euo pipefail
cat -n .dockerignore

Repository: notopia-uit/notopia

Length of output: 1079


🌐 Web query:

In Docker .dockerignore, does a negation rule like !path/to/dirre-include the directory’s contents, or do you need!path/to/dir/**(or!/dir/) to include child files?

💡 Result:

In a.dockerignore file, a simple negation rule like!path/to/dir is generally insufficient to include the directory's contents if the directory itself or its parent paths have been excluded [1][2]. Because Docker's exclusion logic mirrors behavior where parent directories are often skipped entirely for performance reasons, simply negating the directory name does not automatically re-include the files contained within it [3][4][2]. To ensure a directory and all its recursive contents are included, you typically need to use a pattern that matches the directory's contents explicitly, such as!path/to/dir/ [1][5]. If you only use!path/to/dir, Docker may treat that as a command to include the directory entry itself, but it may still refuse to descend into that directory to pick up the files within it because the path was already excluded [1][2]. Using!path/to/dir/ ensures that the pattern matches the directory and all of its sub-contents, effectively overriding previous exclusion rules for the entire tree [5]. Top results: [1][2][5]

Citations:


Re-include the standalone node_modules contents in Docker build context

In .dockerignore, line 3 (!**/.next/standalone/node_modules) only negates the directory path, while line 2 (**/node_modules) still excludes the subtree contents—so the standalone bundle may miss its packaged dependencies.

**/node_modules
!**/.next/standalone/node_modules
Proposed fix
 **/node_modules
 !**/.next/standalone/node_modules
+!**/.next/standalone/node_modules/**
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
**/node_modules
!**/.next/standalone/node_modules
**/node_modules
!**/.next/standalone/node_modules
!**/.next/standalone/node_modules/**
🤖 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 @.dockerignore around lines 2 - 3, The current .dockerignore negation only
matches the directory path and not its contents, so the standalone bundle's
dependencies remain excluded; update the negation to re-include the subtree by
changing "!**/.next/standalone/node_modules" to include contents (for example
"!**/.next/standalone/node_modules/**" and optionally also
"!**/.next/standalone/node_modules") while keeping the general exclusion
"**/node_modules".

Comment on lines +30 to +33
onLoadDocument: async (data) => {
this.logger.debug(
{ documentId: data.documentName, documentMetadata: data.document.getMap('metadata') },
'Document loaded'
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid logging the full document metadata map.

Logging data.document.getMap('metadata') can leak internal/sensitive metadata and produce oversized logs. Please log only bounded, non-sensitive fields (for example, metadata.modified or key count).

Proposed fix
       onLoadDocument: async (data) => {
+        const metadata = data.document.getMap('metadata') as Y.Map<unknown>;
+        const metadataEntry = metadata.get('metadata') as { modified?: boolean } | undefined;
         this.logger.debug(
-          { documentId: data.documentName, documentMetadata: data.document.getMap('metadata') },
+          {
+            documentId: data.documentName,
+            isModified: Boolean(metadataEntry?.modified),
+            metadataKeysCount: metadata.size,
+          },
           'Document loaded'
         );
         await Promise.resolve();
       },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
onLoadDocument: async (data) => {
this.logger.debug(
{ documentId: data.documentName, documentMetadata: data.document.getMap('metadata') },
'Document loaded'
onLoadDocument: async (data) => {
const metadata = data.document.getMap('metadata') as Y.Map<unknown>;
const metadataEntry = metadata.get('metadata') as { modified?: boolean } | undefined;
this.logger.debug(
{
documentId: data.documentName,
isModified: Boolean(metadataEntry?.modified),
metadataKeysCount: metadata.size,
},
'Document loaded'
);
await Promise.resolve();
},
🤖 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 `@apps/document/src/hocuspocus/hocuspocus.ts` around lines 30 - 33, The
onLoadDocument handler is currently logging the full Yjs metadata map
(data.document.getMap('metadata')), which may expose sensitive or oversized
data; update onLoadDocument to extract and log only bounded non-sensitive fields
(e.g. const meta = data.document.getMap('metadata'); const modified =
meta.get('modified'); const keyCount = meta.size or meta.keys().length) and pass
those values to this.logger.debug along with documentId instead of the entire
map; locate the onLoadDocument callback and replace the logger.debug payload
accordingly so only safe, small fields are logged.

<div className="p-4">
<HocuspocusProviderWebsocketComponent
url={`ws://${process.env.NEXT_PUBLIC_API_URL}/document/ws/document`}
url={`ws://${process.env.API_URL}/document/ws/document`}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

WebSocket endpoint construction is protocol-fragile and can produce invalid URLs.

On Line 15, prefixing with ws:// is unsafe: if process.env.API_URL already contains http(s)://, the result is malformed, and HTTPS deployments should use wss://.

Proposed fix
-        url={`ws://${process.env.API_URL}/document/ws/document`}
+        url={(() => {
+          const api = new URL(process.env.API_URL as string);
+          const wsProtocol = api.protocol === 'https:' ? 'wss:' : 'ws:';
+          return `${wsProtocol}//${api.host}/document/ws/document`;
+        })()}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
url={`ws://${process.env.API_URL}/document/ws/document`}
url={(() => {
const api = new URL(process.env.API_URL as string);
const wsProtocol = api.protocol === 'https:' ? 'wss:' : 'ws:';
return `${wsProtocol}//${api.host}/document/ws/document`;
})()}
🤖 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 `@apps/web/app/`(workspace)/workspace/[workspaceId]/note/[noteId]/page.tsx at
line 15, The WebSocket URL construction using
url={`ws://${process.env.API_URL}/document/ws/document`} is protocol‑fragile;
change it to build the ws/wss scheme dynamically from process.env.API_URL (or
parse it with the URL API) so if API_URL already contains a scheme you convert
http->ws and https->wss (or strip any existing scheme and prepend the correct
ws/wss), and ensure the final value passed to the url prop is a valid ws:// or
wss:// URL.

Comment on lines +3 to +11
export function GET() {
const endSessionUrl = process.env.AUTHENTIK_CLIENT_DISCOVERY_URL?.replace(
'/.well-known/openid-configuration',
'/end-session/'
);

const redirectUri = encodeURIComponent(`${process.env.BETTER_AUTH_URL}/signin`);

return NextResponse.redirect(`${endSessionUrl}?post_logout_redirect_uri=${redirectUri}`);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Validate the logout URLs before redirecting.

If AUTHENTIK_CLIENT_DISCOVERY_URL or BETTER_AUTH_URL is missing, this emits a redirect with an invalid target instead of a controlled server error. The same applies if the discovery URL does not contain the expected suffix.

💡 Proposed fix
 export function GET() {
-  const endSessionUrl = process.env.AUTHENTIK_CLIENT_DISCOVERY_URL?.replace(
+  const discoveryUrl = process.env.AUTHENTIK_CLIENT_DISCOVERY_URL;
+  const betterAuthUrl = process.env.BETTER_AUTH_URL;
+
+  if (!discoveryUrl || !betterAuthUrl) {
+    return NextResponse.json({ message: 'Auth logout is not configured' }, { status: 500 });
+  }
+
+  const endSessionUrl = discoveryUrl.replace(
     '/.well-known/openid-configuration',
     '/end-session/'
   );
-
-  const redirectUri = encodeURIComponent(`${process.env.BETTER_AUTH_URL}/signin`);
+  if (endSessionUrl === discoveryUrl) {
+    return NextResponse.json({ message: 'Invalid discovery URL' }, { status: 500 });
+  }
+
+  const redirectUri = encodeURIComponent(`${betterAuthUrl}/signin`);
 
   return NextResponse.redirect(`${endSessionUrl}?post_logout_redirect_uri=${redirectUri}`);
 }
🤖 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 `@apps/web/app/api/auth/logout/route.ts` around lines 3 - 11, The GET handler
builds endSessionUrl and redirectUri from envs without validation; update the
GET function to validate that process.env.AUTHENTIK_CLIENT_DISCOVERY_URL and
process.env.BETTER_AUTH_URL are present and that AUTHENTIK_CLIENT_DISCOVERY_URL
contains '/.well-known/openid-configuration' before calling replace; if
validation fails, return a controlled error response (e.g., NextResponse.json or
NextResponse.redirect to a safe error page) with an appropriate 4xx/5xx status
instead of redirecting to an invalid URL; ensure you reference and check the
endSessionUrl and redirectUri construction paths so the logic short-circuits on
missing/invalid envs.

Comment thread apps/web/app/env-init.tsx
Comment on lines +14 to +22
if (!configured) {
if (!betterAuthUrl) {
console.warn(
'BETTER_AUTH_URL is not set. Falling back to http://localhost:3000'
);
}
configureAuthClient(betterAuthUrl || 'http://localhost:3000');
configured = true;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Move auth client configuration out of EnvInit’s render path.

apps/web/app/env-init.tsx calls configureAuthClient(...) during render (guarded by a module-level configured flag). This makes render impure and can run unexpectedly under React StrictMode/concurrent rendering. Move the configuration into a useEffect (and avoid module-level mutable state) while keeping the existing BETTER_AUTH_URL warning + http://localhost:3000 fallback behaviour.

🤖 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 `@apps/web/app/env-init.tsx` around lines 14 - 22, EnvInit currently calls
configureAuthClient(betterAuthUrl || 'http://localhost:3000') during render
guarded by a module-level configured flag; move that side-effect into a React
useEffect inside the EnvInit component (remove the module-level configured
mutable variable) so configuration runs only after mount and is safe under
StrictMode. Inside the effect, preserve the existing warning when betterAuthUrl
is falsy and call configureAuthClient(betterAuthUrl || 'http://localhost:3000');
use an empty dependency array (or a stable ref) to ensure it only runs once.

Comment on lines +38 to +40
onSseEvent: (raw) => {
handlersRef.current.forEach((handler) => handler(raw.data));
},
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Isolate subscriber failures during fan-out.

One throwing handler currently aborts delivery to the remaining subscribers and can bubble into the SSE path. Wrap each callback individually so a single broken consumer does not take down workspace events for the rest of the app.

Suggested fix
           onSseEvent: (raw) => {
-            handlersRef.current.forEach((handler) => handler(raw.data));
+            handlersRef.current.forEach((handler) => {
+              try {
+                handler(raw.data);
+              } catch (error) {
+                console.error('Workspace event handler failed:', error);
+              }
+            });
           },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
onSseEvent: (raw) => {
handlersRef.current.forEach((handler) => handler(raw.data));
},
onSseEvent: (raw) => {
handlersRef.current.forEach((handler) => {
try {
handler(raw.data);
} catch (error) {
console.error('Workspace event handler failed:', error);
}
});
},
🤖 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 `@packages/ui/src/contexts/workspace-events-context.tsx` around lines 38 - 40,
The fan-out in onSseEvent uses handlersRef.current.forEach((handler) =>
handler(raw.data)) so a throwing handler aborts delivery; change this to call
each handler inside its own try/catch (e.g., iterate handlersRef.current and for
each handler invoke handler(raw.data) inside try { ... } catch (err) { /* log
error */ }) so one subscriber failure doesn't stop others, and log the error
with context including the handler identity or event data.

Comment thread packages/ui/src/hooks/use-is-doc-modified.ts
Comment thread packages/ui/src/hooks/use-is-doc-modified.ts Outdated
Comment on lines 49 to 56
return new Promise((resolve) => {
timeoutRef.current = setTimeout(async () => {
try {
const result = await searchFn(query);
setIsLoading(false);
setError(null);
resolve({ data: result, isLoading: false, error: null });
} catch (err) {
const error = err instanceof Error ? err : new Error('Unknown error occurred');
setError(error);
setIsLoading(false);
resolve({ data: undefined as T, isLoading: false, error });
}
if (inflightQueryRef.current !== query) return;

const result = await doFetch(query);
resolve(result);
}, debounceMs);
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Promise never settles when query becomes stale.

When a newer query supersedes an older one, line 51 returns early without resolving or rejecting the promise. This leaves callers awaiting search() indefinitely blocked, potentially causing memory leaks and unexpected behaviour.

🐛 Proposed fix: resolve with empty/error result for stale queries
       return new Promise((resolve) => {
         timeoutRef.current = setTimeout(async () => {
-          if (inflightQueryRef.current !== query) return;
+          if (inflightQueryRef.current !== query) {
+            resolve({ data: undefined as T, isLoading: false, error: null });
+            return;
+          }

           const result = await doFetch(query);
           resolve(result);
         }, debounceMs);
       });

Alternatively, consider rejecting with a specific error type so callers can distinguish cancellation from actual failures.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return new Promise((resolve) => {
timeoutRef.current = setTimeout(async () => {
try {
const result = await searchFn(query);
setIsLoading(false);
setError(null);
resolve({ data: result, isLoading: false, error: null });
} catch (err) {
const error = err instanceof Error ? err : new Error('Unknown error occurred');
setError(error);
setIsLoading(false);
resolve({ data: undefined as T, isLoading: false, error });
}
if (inflightQueryRef.current !== query) return;
const result = await doFetch(query);
resolve(result);
}, debounceMs);
});
return new Promise((resolve) => {
timeoutRef.current = setTimeout(async () => {
if (inflightQueryRef.current !== query) {
resolve({ data: undefined as T, isLoading: false, error: null });
return;
}
const result = await doFetch(query);
resolve(result);
}, debounceMs);
});
🤖 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 `@packages/ui/src/hooks/use-search-cache.ts` around lines 49 - 56, The returned
Promise in the debounce block (inside use-search-cache's search logic) can
remain unresolved if inflightQueryRef.current !== query; modify that
early-return to settle the Promise by either resolving with a defined "stale"
result (e.g., null or an empty SearchResult) or rejecting with a specific
CancelledError so callers can distinguish cancellation from real failures;
ensure you also clear timeoutRef.current and do any necessary cleanup before
resolving/rejecting, and update callers of search() to handle the chosen
sentinel or error type.

Comment thread packages/ui/src/lib/auth-client.ts
@NTGNguyen NTGNguyen merged commit a174e4a into main May 30, 2026
2 of 3 checks passed
@NTGNguyen NTGNguyen deleted the ntgnguyen/doing-5 branch June 2, 2026 07:15
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.

3 participants