diff --git a/invokeai/app/api/routers/boards.py b/invokeai/app/api/routers/boards.py
index 926c0f7fd22..d5b1acb5144 100644
--- a/invokeai/app/api/routers/boards.py
+++ b/invokeai/app/api/routers/boards.py
@@ -5,7 +5,7 @@
 from pydantic import BaseModel, Field
 
 from invokeai.app.api.dependencies import ApiDependencies
-from invokeai.app.services.board_records.board_records_common import BoardChanges
+from invokeai.app.services.board_records.board_records_common import BoardChanges, UncategorizedImageCounts
 from invokeai.app.services.boards.boards_common import BoardDTO
 from invokeai.app.services.shared.pagination import OffsetPaginatedResults
 
@@ -146,3 +146,14 @@ async def list_all_board_image_names(
         board_id,
     )
     return image_names
+
+
+@boards_router.get(
+    "/uncategorized/counts",
+    operation_id="get_uncategorized_image_counts",
+    response_model=UncategorizedImageCounts,
+)
+async def get_uncategorized_image_counts() -> UncategorizedImageCounts:
+    """Gets count of images and assets for uncategorized images (images with no board assocation)"""
+
+    return ApiDependencies.invoker.services.board_records.get_uncategorized_image_counts()
diff --git a/invokeai/app/services/board_records/board_records_base.py b/invokeai/app/services/board_records/board_records_base.py
index 9d16dacf60b..7bfe6ada6fd 100644
--- a/invokeai/app/services/board_records/board_records_base.py
+++ b/invokeai/app/services/board_records/board_records_base.py
@@ -1,6 +1,6 @@
 from abc import ABC, abstractmethod
 
-from invokeai.app.services.board_records.board_records_common import BoardChanges, BoardRecord
+from invokeai.app.services.board_records.board_records_common import BoardChanges, BoardRecord, UncategorizedImageCounts
 from invokeai.app.services.shared.pagination import OffsetPaginatedResults
 
 
@@ -48,3 +48,8 @@ def get_many(
     def get_all(self, include_archived: bool = False) -> list[BoardRecord]:
         """Gets all board records."""
         pass
+
+    @abstractmethod
+    def get_uncategorized_image_counts(self) -> UncategorizedImageCounts:
+        """Gets count of images and assets for uncategorized images (images with no board assocation)."""
+        pass
diff --git a/invokeai/app/services/board_records/board_records_common.py b/invokeai/app/services/board_records/board_records_common.py
index 0dda8a8b6b6..3478746536f 100644
--- a/invokeai/app/services/board_records/board_records_common.py
+++ b/invokeai/app/services/board_records/board_records_common.py
@@ -1,5 +1,5 @@
 from datetime import datetime
-from typing import Optional, Union
+from typing import Any, Optional, Union
 
 from pydantic import BaseModel, Field
 
@@ -26,21 +26,25 @@ class BoardRecord(BaseModelExcludeNull):
     """Whether or not the board is archived."""
     is_private: Optional[bool] = Field(default=None, description="Whether the board is private.")
     """Whether the board is private."""
+    image_count: int = Field(description="The number of images in the board.")
+    asset_count: int = Field(description="The number of assets in the board.")
 
 
-def deserialize_board_record(board_dict: dict) -> BoardRecord:
+def deserialize_board_record(board_dict: dict[str, Any]) -> BoardRecord:
     """Deserializes a board record."""
 
     # Retrieve all the values, setting "reasonable" defaults if they are not present.
 
     board_id = board_dict.get("board_id", "unknown")
     board_name = board_dict.get("board_name", "unknown")
-    cover_image_name = board_dict.get("cover_image_name", "unknown")
+    cover_image_name = board_dict.get("cover_image_name", None)
     created_at = board_dict.get("created_at", get_iso_timestamp())
     updated_at = board_dict.get("updated_at", get_iso_timestamp())
     deleted_at = board_dict.get("deleted_at", get_iso_timestamp())
     archived = board_dict.get("archived", False)
     is_private = board_dict.get("is_private", False)
+    image_count = board_dict.get("image_count", 0)
+    asset_count = board_dict.get("asset_count", 0)
 
     return BoardRecord(
         board_id=board_id,
@@ -51,6 +55,8 @@ def deserialize_board_record(board_dict: dict) -> BoardRecord:
         deleted_at=deleted_at,
         archived=archived,
         is_private=is_private,
+        image_count=image_count,
+        asset_count=asset_count,
     )
 
 
@@ -63,19 +69,24 @@ class BoardChanges(BaseModel, extra="forbid"):
 class BoardRecordNotFoundException(Exception):
     """Raised when an board record is not found."""
 
-    def __init__(self, message="Board record not found"):
+    def __init__(self, message: str = "Board record not found"):
         super().__init__(message)
 
 
 class BoardRecordSaveException(Exception):
     """Raised when an board record cannot be saved."""
 
-    def __init__(self, message="Board record not saved"):
+    def __init__(self, message: str = "Board record not saved"):
         super().__init__(message)
 
 
 class BoardRecordDeleteException(Exception):
     """Raised when an board record cannot be deleted."""
 
-    def __init__(self, message="Board record not deleted"):
+    def __init__(self, message: str = "Board record not deleted"):
         super().__init__(message)
+
+
+class UncategorizedImageCounts(BaseModel):
+    image_count: int = Field(description="The number of uncategorized images.")
+    asset_count: int = Field(description="The number of uncategorized assets.")
diff --git a/invokeai/app/services/board_records/board_records_sqlite.py b/invokeai/app/services/board_records/board_records_sqlite.py
index c64e060b953..ea02d3e4b21 100644
--- a/invokeai/app/services/board_records/board_records_sqlite.py
+++ b/invokeai/app/services/board_records/board_records_sqlite.py
@@ -9,12 +9,121 @@
     BoardRecordDeleteException,
     BoardRecordNotFoundException,
     BoardRecordSaveException,
+    UncategorizedImageCounts,
     deserialize_board_record,
 )
 from invokeai.app.services.shared.pagination import OffsetPaginatedResults
 from invokeai.app.services.shared.sqlite.sqlite_database import SqliteDatabase
 from invokeai.app.util.misc import uuid_string
 
+_BASE_BOARD_RECORD_QUERY = """
+    -- This query retrieves board records, joining with the board_images and images tables to get image counts and cover image names.
+    -- It is not a complete query, as it is missing a GROUP BY or WHERE clause (and is unterminated).
+    SELECT b.board_id,
+        b.board_name,
+        b.created_at,
+        b.updated_at,
+        b.archived,
+        -- Count the number of images in the board, alias image_count
+        COUNT(
+            CASE
+                WHEN i.image_category in ('general') -- "Images" are images in the 'general' category
+                AND i.is_intermediate = 0 THEN 1 -- Intermediates are not counted
+            END
+        ) AS image_count,
+        -- Count the number of assets in the board, alias asset_count
+        COUNT(
+            CASE
+                WHEN i.image_category in ('control', 'mask', 'user', 'other') -- "Assets" are images in any of the other categories ('control', 'mask', 'user', 'other')
+                AND i.is_intermediate = 0 THEN 1 -- Intermediates are not counted
+            END
+        ) AS asset_count,
+        -- Get the name of the the most recent image in the board, alias cover_image_name
+        (
+            SELECT bi.image_name
+            FROM board_images bi
+                JOIN images i ON bi.image_name = i.image_name
+            WHERE bi.board_id = b.board_id
+                AND i.is_intermediate = 0 -- Intermediates cannot be cover images
+            ORDER BY i.created_at DESC -- Sort by created_at to get the most recent image
+            LIMIT 1
+        ) AS cover_image_name
+    FROM boards b
+        LEFT JOIN board_images bi ON b.board_id = bi.board_id
+        LEFT JOIN images i ON bi.image_name = i.image_name
+    """
+
+
+def get_paginated_list_board_records_queries(include_archived: bool) -> str:
+    """Gets a query to retrieve a paginated list of board records. The query has placeholders for limit and offset.
+
+    Args:
+        include_archived: Whether to include archived board records in the results.
+
+    Returns:
+        A query to retrieve a paginated list of board records.
+    """
+
+    archived_condition = "WHERE b.archived = 0" if not include_archived else ""
+
+    # The GROUP BY must be added _after_ the WHERE clause!
+    query = f"""
+        {_BASE_BOARD_RECORD_QUERY}
+        {archived_condition}
+        GROUP BY b.board_id,
+            b.board_name,
+            b.created_at,
+            b.updated_at
+        ORDER BY b.created_at DESC
+        LIMIT ? OFFSET ?;
+        """
+
+    return query
+
+
+def get_total_boards_count_query(include_archived: bool) -> str:
+    """Gets a query to retrieve the total count of board records.
+
+    Args:
+        include_archived: Whether to include archived board records in the count.
+
+    Returns:
+        A query to retrieve the total count of board records.
+    """
+
+    archived_condition = "WHERE b.archived = 0" if not include_archived else ""
+
+    return f"SELECT COUNT(*) FROM boards {archived_condition};"
+
+
+def get_list_all_board_records_query(include_archived: bool) -> str:
+    """Gets a query to retrieve all board records.
+
+    Args:
+        include_archived: Whether to include archived board records in the results.
+
+    Returns:
+        A query to retrieve all board records.
+    """
+
+    archived_condition = "WHERE b.archived = 0" if not include_archived else ""
+
+    return f"""
+        {_BASE_BOARD_RECORD_QUERY}
+        {archived_condition}
+        GROUP BY b.board_id,
+            b.board_name,
+            b.created_at,
+            b.updated_at
+        ORDER BY b.created_at DESC;
+        """
+
+
+def get_board_record_query() -> str:
+    """Gets a query to retrieve a board record. The query has a placeholder for the board_id."""
+
+    return f"{_BASE_BOARD_RECORD_QUERY} WHERE b.board_id = ?;"
+
 
 class SqliteBoardRecordStorage(BoardRecordStorageBase):
     _conn: sqlite3.Connection
@@ -76,11 +185,7 @@ def get(
         try:
             self._lock.acquire()
             self._cursor.execute(
-                """--sql
-                SELECT *
-                FROM boards
-                WHERE board_id = ?;
-                """,
+                get_board_record_query(),
                 (board_id,),
             )
 
@@ -92,7 +197,7 @@ def get(
             self._lock.release()
         if result is None:
             raise BoardRecordNotFoundException
-        return BoardRecord(**dict(result))
+        return deserialize_board_record(dict(result))
 
     def update(
         self,
@@ -149,45 +254,15 @@ def get_many(
         try:
             self._lock.acquire()
 
-            # Build base query
-            base_query = """
-                SELECT *
-                FROM boards
-                {archived_filter}
-                ORDER BY created_at DESC
-                LIMIT ? OFFSET ?;
-            """
-
-            # Determine archived filter condition
-            if include_archived:
-                archived_filter = ""
-            else:
-                archived_filter = "WHERE archived = 0"
-
-            final_query = base_query.format(archived_filter=archived_filter)
+            main_query = get_paginated_list_board_records_queries(include_archived=include_archived)
 
-            # Execute query to fetch boards
-            self._cursor.execute(final_query, (limit, offset))
+            self._cursor.execute(main_query, (limit, offset))
 
             result = cast(list[sqlite3.Row], self._cursor.fetchall())
             boards = [deserialize_board_record(dict(r)) for r in result]
 
-            # Determine count query
-            if include_archived:
-                count_query = """
-                    SELECT COUNT(*)
-                    FROM boards;
-                """
-            else:
-                count_query = """
-                    SELECT COUNT(*)
-                    FROM boards
-                    WHERE archived = 0;
-                """
-
-            # Execute count query
-            self._cursor.execute(count_query)
-
+            total_query = get_total_boards_count_query(include_archived=include_archived)
+            self._cursor.execute(total_query)
             count = cast(int, self._cursor.fetchone()[0])
 
             return OffsetPaginatedResults[BoardRecord](items=boards, offset=offset, limit=limit, total=count)
@@ -201,26 +276,10 @@ def get_many(
     def get_all(self, include_archived: bool = False) -> list[BoardRecord]:
         try:
             self._lock.acquire()
-
-            base_query = """
-                SELECT *
-                FROM boards
-                {archived_filter}
-                ORDER BY created_at DESC
-            """
-
-            if include_archived:
-                archived_filter = ""
-            else:
-                archived_filter = "WHERE archived = 0"
-
-            final_query = base_query.format(archived_filter=archived_filter)
-
-            self._cursor.execute(final_query)
-
+            query = get_list_all_board_records_query(include_archived=include_archived)
+            self._cursor.execute(query)
             result = cast(list[sqlite3.Row], self._cursor.fetchall())
             boards = [deserialize_board_record(dict(r)) for r in result]
-
             return boards
 
         except sqlite3.Error as e:
@@ -228,3 +287,28 @@ def get_all(self, include_archived: bool = False) -> list[BoardRecord]:
             raise e
         finally:
             self._lock.release()
+
+    def get_uncategorized_image_counts(self) -> UncategorizedImageCounts:
+        try:
+            self._lock.acquire()
+            query = """
+                -- Get the count of uncategorized images and assets.
+                SELECT
+                    CASE
+                        WHEN i.image_category = 'general' THEN 'image_count' -- "Images" are images in the 'general' category
+                        ELSE 'asset_count' -- "Assets" are images in any of the other categories ('control', 'mask', 'user', 'other')
+                    END AS category_type,
+                    COUNT(*) AS unassigned_count
+                FROM images i
+                LEFT JOIN board_images bi ON i.image_name = bi.image_name
+                WHERE bi.board_id IS NULL -- Uncategorized images have no board association
+                AND i.is_intermediate = 0 -- Omit intermediates from the counts
+                GROUP BY category_type; -- Group by category_type alias, as derived from the image_category column earlier
+                """
+            self._cursor.execute(query)
+            results = self._cursor.fetchall()
+            image_count = dict(results)["image_count"]
+            asset_count = dict(results)["asset_count"]
+            return UncategorizedImageCounts(image_count=image_count, asset_count=asset_count)
+        finally:
+            self._lock.release()
diff --git a/invokeai/app/services/boards/boards_common.py b/invokeai/app/services/boards/boards_common.py
index 15d0b3c37f5..1e9337a3edf 100644
--- a/invokeai/app/services/boards/boards_common.py
+++ b/invokeai/app/services/boards/boards_common.py
@@ -1,23 +1,8 @@
-from typing import Optional
-
-from pydantic import Field
-
 from invokeai.app.services.board_records.board_records_common import BoardRecord
 
 
+# TODO(psyche): BoardDTO is now identical to BoardRecord. We should consider removing it.
 class BoardDTO(BoardRecord):
-    """Deserialized board record with cover image URL and image count."""
-
-    cover_image_name: Optional[str] = Field(description="The name of the board's cover image.")
-    """The URL of the thumbnail of the most recent image in the board."""
-    image_count: int = Field(description="The number of images in the board.")
-    """The number of images in the board."""
-
+    """Deserialized board record."""
 
-def board_record_to_dto(board_record: BoardRecord, cover_image_name: Optional[str], image_count: int) -> BoardDTO:
-    """Converts a board record to a board DTO."""
-    return BoardDTO(
-        **board_record.model_dump(exclude={"cover_image_name"}),
-        cover_image_name=cover_image_name,
-        image_count=image_count,
-    )
+    pass
diff --git a/invokeai/app/services/boards/boards_default.py b/invokeai/app/services/boards/boards_default.py
index 97fd3059a93..abf38e8ea71 100644
--- a/invokeai/app/services/boards/boards_default.py
+++ b/invokeai/app/services/boards/boards_default.py
@@ -1,6 +1,6 @@
 from invokeai.app.services.board_records.board_records_common import BoardChanges
 from invokeai.app.services.boards.boards_base import BoardServiceABC
-from invokeai.app.services.boards.boards_common import BoardDTO, board_record_to_dto
+from invokeai.app.services.boards.boards_common import BoardDTO
 from invokeai.app.services.invoker import Invoker
 from invokeai.app.services.shared.pagination import OffsetPaginatedResults
 
@@ -16,17 +16,11 @@ def create(
         board_name: str,
     ) -> BoardDTO:
         board_record = self.__invoker.services.board_records.save(board_name)
-        return board_record_to_dto(board_record, None, 0)
+        return BoardDTO.model_validate(board_record.model_dump())
 
     def get_dto(self, board_id: str) -> BoardDTO:
         board_record = self.__invoker.services.board_records.get(board_id)
-        cover_image = self.__invoker.services.image_records.get_most_recent_image_for_board(board_record.board_id)
-        if cover_image:
-            cover_image_name = cover_image.image_name
-        else:
-            cover_image_name = None
-        image_count = self.__invoker.services.board_image_records.get_image_count_for_board(board_id)
-        return board_record_to_dto(board_record, cover_image_name, image_count)
+        return BoardDTO.model_validate(board_record.model_dump())
 
     def update(
         self,
@@ -34,14 +28,7 @@ def update(
         changes: BoardChanges,
     ) -> BoardDTO:
         board_record = self.__invoker.services.board_records.update(board_id, changes)
-        cover_image = self.__invoker.services.image_records.get_most_recent_image_for_board(board_record.board_id)
-        if cover_image:
-            cover_image_name = cover_image.image_name
-        else:
-            cover_image_name = None
-
-        image_count = self.__invoker.services.board_image_records.get_image_count_for_board(board_id)
-        return board_record_to_dto(board_record, cover_image_name, image_count)
+        return BoardDTO.model_validate(board_record.model_dump())
 
     def delete(self, board_id: str) -> None:
         self.__invoker.services.board_records.delete(board_id)
@@ -50,30 +37,10 @@ def get_many(
         self, offset: int = 0, limit: int = 10, include_archived: bool = False
     ) -> OffsetPaginatedResults[BoardDTO]:
         board_records = self.__invoker.services.board_records.get_many(offset, limit, include_archived)
-        board_dtos = []
-        for r in board_records.items:
-            cover_image = self.__invoker.services.image_records.get_most_recent_image_for_board(r.board_id)
-            if cover_image:
-                cover_image_name = cover_image.image_name
-            else:
-                cover_image_name = None
-
-            image_count = self.__invoker.services.board_image_records.get_image_count_for_board(r.board_id)
-            board_dtos.append(board_record_to_dto(r, cover_image_name, image_count))
-
+        board_dtos = [BoardDTO.model_validate(r.model_dump()) for r in board_records.items]
         return OffsetPaginatedResults[BoardDTO](items=board_dtos, offset=offset, limit=limit, total=len(board_dtos))
 
     def get_all(self, include_archived: bool = False) -> list[BoardDTO]:
         board_records = self.__invoker.services.board_records.get_all(include_archived)
-        board_dtos = []
-        for r in board_records:
-            cover_image = self.__invoker.services.image_records.get_most_recent_image_for_board(r.board_id)
-            if cover_image:
-                cover_image_name = cover_image.image_name
-            else:
-                cover_image_name = None
-
-            image_count = self.__invoker.services.board_image_records.get_image_count_for_board(r.board_id)
-            board_dtos.append(board_record_to_dto(r, cover_image_name, image_count))
-
+        board_dtos = [BoardDTO.model_validate(r.model_dump()) for r in board_records]
         return board_dtos
diff --git a/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/BoardTooltip.tsx b/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/BoardTooltip.tsx
index 63ba2991cfc..e3b30666b0e 100644
--- a/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/BoardTooltip.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/BoardTooltip.tsx
@@ -1,27 +1,23 @@
 import { Flex, Image, Text } from '@invoke-ai/ui-library';
 import { skipToken } from '@reduxjs/toolkit/query';
+import { useMemo } from 'react';
 import { useTranslation } from 'react-i18next';
-import { useGetBoardAssetsTotalQuery, useGetBoardImagesTotalQuery } from 'services/api/endpoints/boards';
 import { useGetImageDTOQuery } from 'services/api/endpoints/images';
-import type { BoardDTO } from 'services/api/types';
 
 type Props = {
-  board: BoardDTO | null;
+  imageCount: number;
+  assetCount: number;
+  isArchived: boolean;
+  coverImageName?: string | null;
 };
 
-export const BoardTooltip = ({ board }: Props) => {
+export const BoardTooltip = ({ imageCount, assetCount, isArchived, coverImageName }: Props) => {
   const { t } = useTranslation();
-  const { imagesTotal } = useGetBoardImagesTotalQuery(board?.board_id || 'none', {
-    selectFromResult: ({ data }) => {
-      return { imagesTotal: data?.total ?? 0 };
-    },
-  });
-  const { assetsTotal } = useGetBoardAssetsTotalQuery(board?.board_id || 'none', {
-    selectFromResult: ({ data }) => {
-      return { assetsTotal: data?.total ?? 0 };
-    },
-  });
-  const { currentData: coverImage } = useGetImageDTOQuery(board?.cover_image_name ?? skipToken);
+  const { currentData: coverImage } = useGetImageDTOQuery(coverImageName ?? skipToken);
+
+  const totalString = useMemo(() => {
+    return `${t('boards.imagesWithCount', { count: imageCount })}, ${t('boards.assetsWithCount', { count: assetCount })}${isArchived ? ` (${t('boards.archived')})` : ''}`;
+  }, [assetCount, imageCount, isArchived, t]);
 
   return (
     
@@ -34,13 +30,11 @@ export const BoardTooltip = ({ board }: Props) => {
           aspectRatio="1/1"
           borderRadius="base"
           borderBottomRadius="lg"
+          mt={1}
         />
       )}
       
-        
-          {t('boards.imagesWithCount', { count: imagesTotal })}, {t('boards.assetsWithCount', { count: assetsTotal })}
-        
-        {board?.archived && ({t('boards.archived')})}
+        {totalString}
       
     
   );
diff --git a/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/GalleryBoard.tsx b/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/GalleryBoard.tsx
index 2bd07ef39cc..75ea1410214 100644
--- a/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/GalleryBoard.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/GalleryBoard.tsx
@@ -119,7 +119,18 @@ const GalleryBoard = ({ board, isSelected }: GalleryBoardProps) => {
   return (
     
       {(ref) => (
-        } openDelay={1000} placement="left" closeOnScroll p={2}>
+        
+          }
+          placement="left"
+          closeOnScroll
+        >
            {
             
             {autoAddBoardId === board.board_id && !editingDisclosure.isOpen && }
             {board.archived && !editingDisclosure.isOpen && }
-            {!editingDisclosure.isOpen && {board.image_count}}
+            {!editingDisclosure.isOpen && {board.image_count + board.asset_count}}
 
             
           
diff --git a/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/NoBoardBoard.tsx b/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/NoBoardBoard.tsx
index 7719c79866f..41d90a30c95 100644
--- a/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/NoBoardBoard.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/NoBoardBoard.tsx
@@ -14,7 +14,7 @@ import {
 import { autoAddBoardIdChanged, boardIdSelected } from 'features/gallery/store/gallerySlice';
 import { memo, useCallback, useMemo } from 'react';
 import { useTranslation } from 'react-i18next';
-import { useGetBoardImagesTotalQuery } from 'services/api/endpoints/boards';
+import { useGetUncategorizedImageCountsQuery } from 'services/api/endpoints/boards';
 import { useBoardName } from 'services/api/hooks/useBoardName';
 
 interface Props {
@@ -27,11 +27,7 @@ const _hover: SystemStyleObject = {
 
 const NoBoardBoard = memo(({ isSelected }: Props) => {
   const dispatch = useAppDispatch();
-  const { imagesTotal } = useGetBoardImagesTotalQuery('none', {
-    selectFromResult: ({ data }) => {
-      return { imagesTotal: data?.total ?? 0 };
-    },
-  });
+  const { data } = useGetUncategorizedImageCountsQuery();
   const autoAddBoardId = useAppSelector(selectAutoAddBoardId);
   const autoAssignBoardOnClick = useAppSelector(selectAutoAssignBoardOnClick);
   const boardSearchText = useAppSelector(selectBoardSearchText);
@@ -60,7 +56,13 @@ const NoBoardBoard = memo(({ isSelected }: Props) => {
   return (
     
       {(ref) => (
-        } openDelay={1000} placement="left" closeOnScroll>
+        
+          }
+          placement="left"
+          closeOnScroll
+        >
            {
               {boardName}
             
             {autoAddBoardId === 'none' && }
-            {imagesTotal}
+            {(data?.image_count ?? 0) + (data?.asset_count ?? 0)}
             
           
         
diff --git a/invokeai/frontend/web/src/services/api/endpoints/boards.ts b/invokeai/frontend/web/src/services/api/endpoints/boards.ts
index 55ebeab3188..2b33a0a603f 100644
--- a/invokeai/frontend/web/src/services/api/endpoints/boards.ts
+++ b/invokeai/frontend/web/src/services/api/endpoints/boards.ts
@@ -1,12 +1,4 @@
-import { ASSETS_CATEGORIES, IMAGE_CATEGORIES } from 'features/gallery/store/types';
-import type {
-  BoardDTO,
-  CreateBoardArg,
-  ListBoardsArgs,
-  OffsetPaginatedResults_ImageDTO_,
-  UpdateBoardArg,
-} from 'services/api/types';
-import { getListImagesUrl } from 'services/api/util';
+import type { BoardDTO, CreateBoardArg, ListBoardsArgs, S, UpdateBoardArg } from 'services/api/types';
 
 import type { ApiTagDescription } from '..';
 import { api, buildV1Url, LIST_TAG } from '..';
@@ -55,38 +47,11 @@ export const boardsApi = api.injectEndpoints({
       keepUnusedDataFor: 0,
     }),
 
-    getBoardImagesTotal: build.query<{ total: number }, string | undefined>({
-      query: (board_id) => ({
-        url: getListImagesUrl({
-          board_id: board_id ?? 'none',
-          categories: IMAGE_CATEGORIES,
-          is_intermediate: false,
-          limit: 0,
-          offset: 0,
-        }),
-        method: 'GET',
+    getUncategorizedImageCounts: build.query({
+      query: () => ({
+        url: buildBoardsUrl('uncategorized/counts'),
       }),
-      providesTags: (result, error, arg) => [{ type: 'BoardImagesTotal', id: arg ?? 'none' }, 'FetchOnReconnect'],
-      transformResponse: (response: OffsetPaginatedResults_ImageDTO_) => {
-        return { total: response.total };
-      },
-    }),
-
-    getBoardAssetsTotal: build.query<{ total: number }, string | undefined>({
-      query: (board_id) => ({
-        url: getListImagesUrl({
-          board_id: board_id ?? 'none',
-          categories: ASSETS_CATEGORIES,
-          is_intermediate: false,
-          limit: 0,
-          offset: 0,
-        }),
-        method: 'GET',
-      }),
-      providesTags: (result, error, arg) => [{ type: 'BoardAssetsTotal', id: arg ?? 'none' }, 'FetchOnReconnect'],
-      transformResponse: (response: OffsetPaginatedResults_ImageDTO_) => {
-        return { total: response.total };
-      },
+      providesTags: ['UncategorizedImageCounts', { type: 'Board', id: LIST_TAG }, { type: 'Board', id: 'none' }],
     }),
 
     /**
@@ -124,9 +89,8 @@ export const boardsApi = api.injectEndpoints({
 
 export const {
   useListAllBoardsQuery,
-  useGetBoardImagesTotalQuery,
-  useGetBoardAssetsTotalQuery,
   useCreateBoardMutation,
   useUpdateBoardMutation,
   useListAllImageNamesForBoardQuery,
+  useGetUncategorizedImageCountsQuery,
 } = boardsApi;
diff --git a/invokeai/frontend/web/src/services/api/index.ts b/invokeai/frontend/web/src/services/api/index.ts
index 0b82714d94c..1fa78cc4d7e 100644
--- a/invokeai/frontend/web/src/services/api/index.ts
+++ b/invokeai/frontend/web/src/services/api/index.ts
@@ -46,6 +46,7 @@ const tagTypes = [
   // This is invalidated on reconnect. It should be used for queries that have changing data,
   // especially related to the queue and generation.
   'FetchOnReconnect',
+  'UncategorizedImageCounts',
 ] as const;
 export type ApiTagDescription = TagDescription<(typeof tagTypes)[number]>;
 export const LIST_TAG = 'LIST';
diff --git a/invokeai/frontend/web/src/services/api/schema.ts b/invokeai/frontend/web/src/services/api/schema.ts
index b34e9589a4c..e483d678759 100644
--- a/invokeai/frontend/web/src/services/api/schema.ts
+++ b/invokeai/frontend/web/src/services/api/schema.ts
@@ -785,6 +785,26 @@ export type paths = {
         patch?: never;
         trace?: never;
     };
+    "/api/v1/boards/uncategorized/counts": {
+        parameters: {
+            query?: never;
+            header?: never;
+            path?: never;
+            cookie?: never;
+        };
+        /**
+         * Get Uncategorized Image Counts
+         * @description Gets count of images and assets for uncategorized images (images with no board assocation)
+         */
+        get: operations["get_uncategorized_image_counts"];
+        put?: never;
+        post?: never;
+        delete?: never;
+        options?: never;
+        head?: never;
+        patch?: never;
+        trace?: never;
+    };
     "/api/v1/board_images/": {
         parameters: {
             query?: never;
@@ -1973,7 +1993,7 @@ export type components = {
         };
         /**
          * BoardDTO
-         * @description Deserialized board record with cover image URL and image count.
+         * @description Deserialized board record.
          */
         BoardDTO: {
             /**
@@ -2003,9 +2023,9 @@ export type components = {
             deleted_at?: string | null;
             /**
              * Cover Image Name
-             * @description The name of the board's cover image.
+             * @description The name of the cover image of the board.
              */
-            cover_image_name: string | null;
+            cover_image_name?: string | null;
             /**
              * Archived
              * @description Whether or not the board is archived.
@@ -2021,6 +2041,11 @@ export type components = {
              * @description The number of images in the board.
              */
             image_count: number;
+            /**
+             * Asset Count
+             * @description The number of assets in the board.
+             */
+            asset_count: number;
         };
         /**
          * BoardField
@@ -4345,7 +4370,7 @@ export type components = {
         };
         /**
          * Core Metadata
-         * @description Collects core generation metadata into a MetadataField
+         * @description Used internally by Invoke to collect metadata for generations.
          */
         CoreMetadataInvocation: {
             /**
@@ -16391,6 +16416,19 @@ export type components = {
              */
             type: "url";
         };
+        /** UncategorizedImageCounts */
+        UncategorizedImageCounts: {
+            /**
+             * Image Count
+             * @description The number of uncategorized images.
+             */
+            image_count: number;
+            /**
+             * Asset Count
+             * @description The number of uncategorized assets.
+             */
+            asset_count: number;
+        };
         /**
          * Unsharp Mask
          * @description Applies an unsharp mask filter to an image
@@ -18845,6 +18883,26 @@ export interface operations {
             };
         };
     };
+    get_uncategorized_image_counts: {
+        parameters: {
+            query?: never;
+            header?: never;
+            path?: never;
+            cookie?: never;
+        };
+        requestBody?: never;
+        responses: {
+            /** @description Successful Response */
+            200: {
+                headers: {
+                    [name: string]: unknown;
+                };
+                content: {
+                    "application/json": components["schemas"]["UncategorizedImageCounts"];
+                };
+            };
+        };
+    };
     add_image_to_board: {
         parameters: {
             query?: never;
diff --git a/invokeai/frontend/web/src/services/api/types.ts b/invokeai/frontend/web/src/services/api/types.ts
index 647f141f4f6..b0860d6678c 100644
--- a/invokeai/frontend/web/src/services/api/types.ts
+++ b/invokeai/frontend/web/src/services/api/types.ts
@@ -37,7 +37,6 @@ export type AppDependencyVersions = S['AppDependencyVersions'];
 export type ImageDTO = S['ImageDTO'];
 export type BoardDTO = S['BoardDTO'];
 export type ImageCategory = S['ImageCategory'];
-export type OffsetPaginatedResults_ImageDTO_ = S['OffsetPaginatedResults_ImageDTO_'];
 
 // Models
 export type ModelType = S['ModelType'];
diff --git a/invokeai/frontend/web/src/services/events/onInvocationComplete.tsx b/invokeai/frontend/web/src/services/events/onInvocationComplete.tsx
index f6fae1ec4a8..fc16c01cdc8 100644
--- a/invokeai/frontend/web/src/services/events/onInvocationComplete.tsx
+++ b/invokeai/frontend/web/src/services/events/onInvocationComplete.tsx
@@ -6,7 +6,6 @@ import { stagingAreaImageStaged } from 'features/controlLayers/store/canvasStagi
 import { boardIdSelected, galleryViewChanged, imageSelected, offsetChanged } from 'features/gallery/store/gallerySlice';
 import { $nodeExecutionStates, upsertExecutionState } from 'features/nodes/hooks/useExecutionState';
 import { zNodeStatus } from 'features/nodes/types/invocation';
-import { boardsApi } from 'services/api/endpoints/boards';
 import { getImageDTOSafe, imagesApi } from 'services/api/endpoints/images';
 import type { ImageDTO, S } from 'services/api/types';
 import { getCategories, getListImagesUrl } from 'services/api/util';
@@ -31,13 +30,6 @@ export const buildOnInvocationComplete = (getState: () => RootState, dispatch: A
       return;
     }
 
-    // update the total images for the board
-    dispatch(
-      boardsApi.util.updateQueryData('getBoardImagesTotal', imageDTO.board_id ?? 'none', (draft) => {
-        draft.total += 1;
-      })
-    );
-
     dispatch(
       imagesApi.util.invalidateTags([
         { type: 'Board', id: imageDTO.board_id ?? 'none' },
diff --git a/tests/app/services/bulk_download/test_bulk_download.py b/tests/app/services/bulk_download/test_bulk_download.py
index 223ecc88632..48842d9a4bd 100644
--- a/tests/app/services/bulk_download/test_bulk_download.py
+++ b/tests/app/services/bulk_download/test_bulk_download.py
@@ -127,7 +127,16 @@ def test_generate_id_with_board_id(monkeypatch: Any, mock_invoker: Invoker):
 
     def mock_board_get(*args, **kwargs):
         return BoardRecord(
-            board_id="12345", board_name="test_board_name", created_at="None", updated_at="None", archived=False
+            board_id="12345",
+            board_name="test_board_name",
+            created_at="None",
+            updated_at="None",
+            archived=False,
+            asset_count=0,
+            image_count=0,
+            cover_image_name="asdf.png",
+            deleted_at=None,
+            is_private=False,
         )
 
     monkeypatch.setattr(mock_invoker.services.board_records, "get", mock_board_get)
@@ -156,7 +165,16 @@ def test_handler_board_id(tmp_path: Path, monkeypatch: Any, mock_image_dto: Imag
 
     def mock_board_get(*args, **kwargs):
         return BoardRecord(
-            board_id="12345", board_name="test_board_name", created_at="None", updated_at="None", archived=False
+            board_id="12345",
+            board_name="test_board_name",
+            created_at="None",
+            updated_at="None",
+            archived=False,
+            asset_count=0,
+            image_count=0,
+            cover_image_name="asdf.png",
+            deleted_at=None,
+            is_private=False,
         )
 
     monkeypatch.setattr(mock_invoker.services.board_records, "get", mock_board_get)