diff --git a/migrations/sqlite/20260522000000_add_records_collection_indexed_at_index.sql b/migrations/sqlite/20260522000000_add_records_collection_indexed_at_index.sql new file mode 100644 --- /dev/null +++ b/migrations/sqlite/20260522000000_add_records_collection_indexed_at_index.sql @@ -0,0 +1,2 @@ +CREATE INDEX IF NOT EXISTS idx_records_collection_indexed_at +ON records (collection, indexed_at DESC); diff --git a/src/admin/mod.rs b/src/admin/mod.rs --- a/src/admin/mod.rs +++ b/src/admin/mod.rs @@ -71,6 +71,7 @@ "/records", get(records::list_records).delete(records::delete_record), ) + .route("/records/collections", get(records::list_collections)) .route( "/records/collection", delete(records::delete_collection_records), diff --git a/src/admin/records.rs b/src/admin/records.rs --- a/src/admin/records.rs +++ b/src/admin/records.rs @@ -219,3 +219,23 @@ Ok(StatusCode::NO_CONTENT) } + +/// GET /admin/records/collections — list collection names (fast, no record counting). +pub(super) async fn list_collections( + State(state): State, + auth: UserAuth, +) -> Result, AppError> { + auth.require(Permission::RecordsRead).await?; + + let sql = adapt_sql( + "SELECT id FROM lexicons WHERE json_extract(lexicon_json, '$.defs.main.type') = 'record' ORDER BY id", + state.db_backend, + ); + let rows: Vec<(String,)> = sqlx::query_as(&sql) + .fetch_all(&state.db) + .await + .map_err(|e| AppError::Internal(format!("failed to list collections: {e}")))?; + + let collections: Vec<&str> = rows.iter().map(|(id,)| id.as_str()).collect(); + Ok(Json(serde_json::json!({ "collections": collections }))) +} diff --git a/web/src/app/dashboard/records/page.tsx b/web/src/app/dashboard/records/page.tsx --- a/web/src/app/dashboard/records/page.tsx +++ b/web/src/app/dashboard/records/page.tsx @@ -12,12 +12,11 @@ import { useSearchParams } from "next/navigation"; import { useCurrentUser } from "@/hooks/use-current-user"; import { - getStats, + getCollections, getAdminRecords, deleteRecord, deleteCollectionRecords, } from "@/lib/api"; -import type { CollectionStat } from "@/types/stats"; import type { AdminRecord } from "@/types/records"; import { ResponsiveDialog, @@ -83,7 +82,7 @@ const searchParams = useSearchParams(); const initialCollection = searchParams.get("collection") ?? ""; const appliedInitial = useRef(false); - const [collections, setCollections] = useState([]); + const [collections, setCollections] = useState([]); const [selectedCollection, setSelectedCollection] = useState(""); const [records, setRecords] = useState([]); const [cursorStack, setCursorStack] = useState([]); @@ -104,8 +103,8 @@ const [rowSelection, setRowSelection] = useState({}); useEffect(() => { - getStats() - .then((stats) => setCollections(stats.collections)) + getCollections() + .then((data) => setCollections(data.collections)) .catch((e) => setError(e.message)); }, []); @@ -113,7 +112,7 @@ useEffect(() => { if (appliedInitial.current || !initialCollection || collections.length === 0) return; - if (collections.some((c) => c.collection === initialCollection)) { + if (collections.includes(initialCollection)) { appliedInitial.current = true; handleSelectCollection(initialCollection); } @@ -171,9 +170,9 @@ setBulkDeleteMode("selected"); setBulkDeleteConfirm(""); setRowSelection({}); - // Refresh stats and records - const stats = await getStats(); - setCollections(stats.collections); + // Refresh collections and records + const data = await getCollections(); + setCollections(data.collections); setCursorStack([]); setNextCursor(undefined); setRecords([]); @@ -369,8 +368,8 @@ {collections.map((col) => ( - - {col.collection} + + {col} ))} @@ -567,67 +566,6 @@ {(() => { const selectedCount = Object.keys(rowSelection).length; - const totalCount = - collections.find((c) => c.collection === selectedCollection) - ?.count ?? 0; - const allInCollection = - table.getIsAllPageRowsSelected() && - selectedCount >= totalCount; - - if (allInCollection) { - return ( - <> - - - Delete all records in collection? - - - This will permanently delete all {totalCount} record(s) - in{" "} - - {selectedCollection} - - . This action cannot be undone. - - -
- - setBulkDeleteConfirm(e.target.value)} - placeholder={selectedCollection} - /> -
- - - - - - - - ); - } if (!table.getIsAllPageRowsSelected()) { return (