user-context migration
CI / build (push) Has been skipped
CI / deploy (push) Successful in 1m21s

This commit is contained in:
2026-03-24 18:46:36 -05:00
parent d33a838aec
commit 52786dcd44
8 changed files with 12590 additions and 108 deletions
+200 -108
View File
@@ -217,8 +217,9 @@ export const startContextPolling = () => {
try {
// Fetch only recent updates (last 30s) to minimize payload
const timeStr = new Date(Date.now() - 30000).toISOString();
const contextFilter = buildPersonalAccessFilter([personalOrUserId]);
const records = await pb.collection(TASGRID_COLLECTION).getFullList({
filter: `${relationFilter("user", personalOrUserId)} && updated >= "${timeStr}"`,
filter: `${contextFilter} && updated >= "${timeStr}"`,
sort: '-updated',
fields: getTaskListFields(),
requestKey: 'context-poll'
@@ -286,6 +287,7 @@ interface ScopedTaskgridPrefs {
collapsedUntilUpdatedTasks?: Record<string, string>;
migratedStructuredTagsV2?: boolean;
migratedPersonalContextRefsV1?: boolean;
migratedContextOwnershipV2?: boolean;
lastCompletedTaskCleanupDate?: string; // YYYY-MM-DD, per user/environment
}
@@ -558,14 +560,14 @@ export const getNoteSharePolicy = (note: Pick<Note, "tags"> | null | undefined):
note?.tags?.includes(NOTE_HANDOFF_POLICY_TAG) ? "handoff" : "collaborative";
export const getTaskSharePolicy = (ref: Pick<TaskShareRef, "kind" | "key" | "policy" | "contextId">): "collaborative" | "handoff" => {
if (ref.policy) return ref.policy;
if (ref.kind === "note") {
if (ref.policy) return ref.policy;
const note = store.notes.find(existing => existing.id === ref.contextId || existing.key === ref.key);
return getNoteSharePolicy(note);
}
const context = getContextByRef(ref as TaskShareRef);
if (ref.policy) return ref.policy;
if (context?.policy) return context.policy;
return DEFAULT_SHARE_POLICY_BY_KIND[ref.kind];
@@ -868,6 +870,106 @@ const getPersonalContext = (userId = pb.authStore.model?.id) =>
) || null
: null;
const getCurrentUserDisplayName = () =>
(pb.authStore.model?.name || pb.authStore.model?.email || "").trim() || null;
const getPersonalContextTag = (userId = pb.authStore.model?.id) => {
const context = getPersonalContext(userId);
if (context?.displayName) {
return buildShareTag(context.displayName);
}
if (userId && userId === pb.authStore.model?.id) {
const displayName = getCurrentUserDisplayName();
if (displayName) {
return buildShareTag(displayName);
}
}
return null;
};
const escapeFilterValue = (value: string) => value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
const buildPersonalAccessFilter = (userIds: string[], includeLegacyUser = true) => {
const clauses = userIds.flatMap(userId => {
const nextClauses: string[] = [];
const personalTag = getPersonalContextTag(userId);
if (personalTag) {
nextClauses.push(`tags ~ "${escapeFilterValue(personalTag)}"`);
}
if (includeLegacyUser) {
nextClauses.push(relationFilter("user", userId));
}
return nextClauses;
}).filter(Boolean);
return clauses.length > 0 ? `(${clauses.join(" || ")})` : "false";
};
const getPersonalContextUserIdFromRef = (ref: TaskShareRef) => {
if (ref.kind !== "user") return null;
const context = getContextByRef(ref);
if (context?.targetUserId) {
return context.targetUserId;
}
if (ref.key.startsWith("user-")) {
return ref.key.replace(/^user-/, "") || null;
}
return null;
};
const getTaskPersonalRefs = (task: Pick<Task, "shareRefs">) =>
task.shareRefs.filter(ref => ref.kind === "user" && !!getPersonalContextUserIdFromRef(ref));
const getTaskPersonalUserIds = (task: Pick<Task, "shareRefs">) =>
[...new Set(getTaskPersonalRefs(task).map(ref => getPersonalContextUserIdFromRef(ref)).filter((value): value is string => !!value))];
const taskHasLegacyPersonalAccess = (task: Pick<Task, "ownerId" | "shareRefs">, userId: string) =>
task.ownerId === userId &&
getTaskPersonalRefs(task).length === 0 &&
!task.shareRefs.some(ref => ref.kind !== "user" && getTaskSharePolicy(ref) === "handoff");
const taskHasPersonalContextAccess = (task: Pick<Task, "ownerId" | "shareRefs">, userId: string, allowLegacyFallback = true) =>
getTaskPersonalUserIds(task).includes(userId) ||
(allowLegacyFallback && taskHasLegacyPersonalAccess(task, userId));
const resolveMirroredOwnerId = (
currentOwnerId: string | null | undefined,
shareRefs: TaskShareRef[],
preferredOwnerId?: string | null
) => {
const personalUserIds = [...new Set(
shareRefs
.filter(ref => ref.kind === "user")
.map(ref => getPersonalContextUserIdFromRef(ref))
.filter((value): value is string => !!value)
)];
if (personalUserIds.length === 1) {
return personalUserIds[0];
}
if (personalUserIds.length > 1 && preferredOwnerId && personalUserIds.includes(preferredOwnerId)) {
return preferredOwnerId;
}
return currentOwnerId || null;
};
const withMirroredOwnerId = (
currentOwnerId: string | null | undefined,
shareRefs: TaskShareRef[],
updates: Partial<Task>,
preferredOwnerId?: string | null
) => ({
...updates,
ownerId: resolveMirroredOwnerId(currentOwnerId, shareRefs, preferredOwnerId)
});
export const isCurrentUserContextTag = (tag: string) => {
const normalizedTag = tag.trim().toLowerCase();
if (!normalizedTag.startsWith("@")) return false;
@@ -937,69 +1039,34 @@ export const matchesFilter = (task: Task) => {
const activeShareContexts = task.shareRefs
.map(ref => getContextByRef(ref))
.filter((context): context is ShareContext => !!context && !context.deletedAt);
const collaborativeUserShare = task.shareRefs.some(ref => {
if (ref.kind !== "user" || getTaskSharePolicy(ref) !== "collaborative") return false;
const context = getContextByRef(ref);
return context?.targetUserId === currentUserId;
});
const bucketRefs = activeShareContexts.filter(context => context.kind === "bucket");
const hasHandoffContext = task.shareRefs.some(ref =>
ref.kind !== "user" && getTaskSharePolicy(ref) === "handoff"
);
const inPinnedBucket = rawBucketRefs.some(ref =>
store.subscribedBuckets.includes(ref.contextId) ||
store.subscribedBuckets.some(bucketId => store.contexts.find(context => context.id === bucketId)?.key === ref.key)
);
const inAnyBucket = rawBucketRefs.length > 0;
const isPersonalView = ctx === "mine" || (
typeof ctx === "object" &&
"bucketId" in ctx &&
!!personalContext &&
ctx.bucketId === personalContext.id
);
const inPersonalContext = !!personalContext && task.shareRefs.some(ref =>
ref.kind === "user" &&
(
ref.contextId === personalContext.id ||
ref.key === personalContext.key ||
normalizeContextKey(personalContext.displayName) === ref.key
)
);
// 0. Context Filter
if (isPersonalView) {
if (hasHandoffContext) return false;
const isOwner = task.ownerId === currentUserId;
if (!isOwner && !collaborativeUserShare && !inPersonalContext) return false;
if (!currentUserId || !taskHasPersonalContextAccess(task, currentUserId)) return false;
} else if (typeof ctx === 'object' && 'bucketId' in ctx) {
const activeBucketContext = store.contexts.find(context => context.id === (ctx as { bucketId: string }).bucketId);
const activeBucketKey = activeBucketContext?.key || normalizeContextKey((ctx as { name: string }).name);
const inCurrentBucket = rawBucketRefs.some(ref =>
ref.contextId === (ctx as { bucketId: string }).bucketId ||
ref.key === activeBucketKey
) || bucketRefs.some(context => context.id === (ctx as { bucketId: string }).bucketId);
const inCurrentBucket = activeBucketContext?.kind === "user"
? !!activeBucketContext.targetUserId && taskHasPersonalContextAccess(task, activeBucketContext.targetUserId)
: rawBucketRefs.some(ref =>
ref.contextId === (ctx as { bucketId: string }).bucketId ||
ref.key === activeBucketKey
) || bucketRefs.some(context => context.id === (ctx as { bucketId: string }).bucketId);
if (!inCurrentBucket) return false;
} else {
const targetUserId = (ctx as { userId: string }).userId;
const targetPersonalContext = getPersonalContext(targetUserId);
const inTargetPersonalContext = !!targetPersonalContext && task.shareRefs.some(ref =>
ref.kind === "user" &&
(
ref.contextId === targetPersonalContext.id ||
ref.key === targetPersonalContext.key ||
normalizeContextKey(targetPersonalContext.displayName) === ref.key
)
);
if (targetPersonalContext) {
if (!inTargetPersonalContext) return false;
} else if (task.ownerId !== targetUserId) {
if (!taskHasPersonalContextAccess(task, targetUserId)) {
return false;
}
}
if (isPersonalView && inAnyBucket && !inPinnedBucket && task.ownerId !== currentUserId) return false;
// Hide templates from all regular views
if (task.tags?.includes("__template__")) return false;
@@ -1046,7 +1113,7 @@ export const matchesFilter = (task: Task) => {
}
// Owned By Me
if (f.ownedByMe && task.ownerId !== currentUserId) {
if (f.ownedByMe && (!currentUserId || !taskHasPersonalContextAccess(task, currentUserId))) {
return false;
}
@@ -1107,7 +1174,7 @@ createRoot(() => {
// Calculate Top 20 Tasks
const incompleteTasks = store.tasks.filter(t =>
!t.completed &&
t.ownerId === pb.authStore.model?.id &&
taskHasPersonalContextAccess(t, pb.authStore.model?.id || "", true) &&
!t.tags?.includes("__template__") &&
!t.deletedAt
);
@@ -1306,7 +1373,9 @@ const buildTaskMigrationPayload = (
const ownerContext = task.ownerId ? ownerContextByUserId.get(task.ownerId) : undefined;
let nextShareRefs = dedupeShareRefs(task.shareRefs);
let nextNoteRefs = dedupeNoteRefs([...task.noteRefs, ...linkedNoteRefs]);
const hasHandoffContext = nextShareRefs.some(ref => ref.kind !== "user" && getTaskSharePolicy(ref) === "handoff");
const shouldHideFromLegacyOwner = !!task.ownerId &&
getTaskPersonalRefs(task).length === 0 &&
nextShareRefs.some(ref => ref.kind !== "user" && getTaskSharePolicy(ref) === "handoff");
if (ownerContext) {
const matchesOwnerContext = (ref: TaskShareRef) =>
@@ -1316,7 +1385,7 @@ const buildTaskMigrationPayload = (
normalizeContextKey(ownerContext.displayName) === ref.key
);
if (hasHandoffContext) {
if (shouldHideFromLegacyOwner) {
nextShareRefs = nextShareRefs.filter(ref => !matchesOwnerContext(ref));
} else if (!nextShareRefs.some(matchesOwnerContext)) {
nextShareRefs = [
@@ -1332,12 +1401,18 @@ const buildTaskMigrationPayload = (
}
nextShareRefs = dedupeShareRefs(nextShareRefs);
const mirroredOwnerId = resolveMirroredOwnerId(
task.ownerId || fallbackUserId || pb.authStore.model?.id || null,
nextShareRefs,
task.ownerId || fallbackUserId || null
);
return {
tags: buildTaskTags(task.labelTags, nextShareRefs, nextNoteRefs, favoriteTag),
labelTags: task.labelTags,
shareRefs: nextShareRefs,
noteRefs: nextNoteRefs,
ownerId: mirroredOwnerId,
createdBy: task.createdBy || task.ownerId || fallbackUserId || pb.authStore.model?.id || ""
};
};
@@ -1612,10 +1687,22 @@ export const loadNoteContent = async (id: string) => {
// Helper to check if a task should be visible based on ownership, shares, or rules
const shouldTaskBeVisible = (task: any, currentUserId: string): boolean => {
const mappedTask = "shareRefs" in task ? task as Task : mapRecordToTask(task);
if (mappedTask.ownerId === currentUserId) return true;
if (store.supervisedContexts.some(context => context.ownerUserId === mappedTask.ownerId)) return true;
if (taskHasPersonalContextAccess(mappedTask, currentUserId)) return true;
if (store.supervisedContexts.some(context => taskHasPersonalContextAccess(mappedTask, context.ownerUserId))) return true;
if (mappedTask.shareRefs.some(ref => ref.kind === "bucket")) return true;
const hasSubscribedBucket = mappedTask.shareRefs.some(ref =>
ref.kind === "bucket" && (
store.subscribedBuckets.includes(ref.contextId) ||
store.subscribedBuckets.some(bucketId => store.contexts.find(context => context.id === bucketId)?.key === ref.key)
)
);
if (hasSubscribedBucket) return true;
const hasAccessibleNote = mappedTask.noteRefs.some(ref => {
const note = getNoteByRef(ref);
return !!note && (!note.isPrivate || note.user === currentUserId);
});
if (hasAccessibleNote) return true;
return mappedTask.shareRefs.some(ref => {
if (ref.kind !== "user" || getTaskSharePolicy(ref) !== "collaborative") return false;
@@ -1879,7 +1966,7 @@ const syncUserContexts = async () => {
key: buildUserContextKey(user.id),
displayName,
kind: "user",
policy: "handoff",
policy: "collaborative",
targetUserId: user.id
}, { requestKey: null }).catch(() => null);
if (created) {
@@ -1933,7 +2020,7 @@ const syncSupervisedContexts = async () => {
}
};
const runPersonalContextMigration = async (force = false) => {
const runContextOwnershipMigration = async (force = false) => {
const currentUserId = pb.authStore.model?.id;
if (!currentUserId) return;
@@ -1942,19 +2029,23 @@ const runPersonalContextMigration = async (force = false) => {
const rawPrefs = userRec.Taskgrid_pref || {};
const prefs = readScopedPrefs(rawPrefs);
if (!force && prefs.migratedPersonalContextRefsV1) {
if (!force && prefs.migratedContextOwnershipV2) {
return;
}
const contexts = store.contexts.length > 0
? store.contexts
: await loadContextsIntoStore();
await syncUserContexts();
const contexts = store.contexts.length > 0 ? store.contexts : await loadContextsIntoStore();
const ownerContextByUserId = new Map(
contexts
.filter(context => context.kind === "user" && !!context.targetUserId && !context.deletedAt)
.map(context => [context.targetUserId as string, context])
);
const noteLinksByTaskId = buildNoteLinkMap(store.notes);
const noteRecords = await pb.collection(NOTES_COLLECTION).getFullList({
fields: NOTE_LIST_FIELDS,
requestKey: null
}).catch(() => []);
const allNotes = noteRecords.map(record => mapRecordToNote(record));
const noteLinksByTaskId = buildNoteLinkMap(allNotes);
const taskRecords = await pb.collection(TASGRID_COLLECTION).getFullList({
fields: getTaskListFields(),
@@ -1976,19 +2067,27 @@ const runPersonalContextMigration = async (force = false) => {
const sameShareRefs = JSON.stringify(payload.shareRefs) === JSON.stringify(task.shareRefs);
const sameNoteRefs = JSON.stringify(payload.noteRefs) === JSON.stringify(task.noteRefs);
const sameTags = JSON.stringify(payload.tags) === JSON.stringify(task.tags);
if (sameShareRefs && sameNoteRefs && sameTags && payload.createdBy === task.createdBy) continue;
const sameOwnerId = payload.ownerId === task.ownerId;
if (sameShareRefs && sameNoteRefs && sameTags && payload.createdBy === task.createdBy && sameOwnerId) continue;
await pb.collection(TASGRID_COLLECTION).update(task.id, payload, { requestKey: null });
const pbPayload = {
...payload,
user: payload.ownerId || task.ownerId || null
} as any;
delete pbPayload.ownerId;
await pb.collection(TASGRID_COLLECTION).update(task.id, pbPayload, { requestKey: null });
}
await pb.collection('users').update(currentUserId, {
Taskgrid_pref: writeScopedPrefs(rawPrefs, {
...prefs,
migratedPersonalContextRefsV1: true
migratedPersonalContextRefsV1: true,
migratedContextOwnershipV2: true
})
}, { requestKey: null });
} catch (err) {
console.error("[MIGRATION] Failed personal context backfill:", err);
console.error("[MIGRATION] Failed context ownership migration:", err);
}
};
@@ -2001,7 +2100,7 @@ export const runDevDataMigration = async () => {
await toast.promise((async () => {
await runLegacyTagMigration(true);
await runPersonalContextMigration(true);
await runContextOwnershipMigration(true);
await loadContextsIntoStore();
await syncSupervisedContexts();
return "Dev data migration completed.";
@@ -2027,7 +2126,7 @@ export const runProdDataMigration = async () => {
await toast.promise((async () => {
await runLegacyTagMigration(true);
await runPersonalContextMigration(true);
await runContextOwnershipMigration(true);
await loadContextsIntoStore();
await syncSupervisedContexts();
return "Production data migration completed.";
@@ -2065,8 +2164,9 @@ export const initStore = async () => {
res.sort((a, b) => cachedQuickload.indexOf(a.id) - cachedQuickload.indexOf(b.id));
focusRecords = { items: res };
} else {
const personalAccessFilter = buildPersonalAccessFilter([currentUserId]);
focusRecords = await pb.collection(TASGRID_COLLECTION).getList(1, 30, {
filter: `${relationFilter("user", currentUserId)} && completed = false && deletedAt = ""`,
filter: `${personalAccessFilter} && completed = false && deletedAt = ""`,
sort: '-priority,dueDate',
requestKey: null
});
@@ -2206,18 +2306,13 @@ export const initStore = async () => {
const subscribedBucketTags = getSubscribedBucketContexts()
.map(context => buildShareTag(context.displayName));
const supervisedUserIds = store.supervisedContexts.map(context => context.ownerUserId);
const collaborativeKeys = store.contexts
.filter(context =>
!context.deletedAt &&
context.kind === "user" &&
context.targetUserId === currentUserId
)
.map(context => buildShareTag(context.displayName));
const currentUserAccessFilter = buildPersonalAccessFilter(currentUserId ? [currentUserId] : []);
const supervisedAccessFilter = buildPersonalAccessFilter(supervisedUserIds);
// 2a. Fetch ALL Incomplete Tasks (Metadata Only) to ensure availability
const incompletePromises = [
pb.collection(TASGRID_COLLECTION).getFullList({
filter: `${relationFilter("user", currentUserId)} && completed = false`,
filter: `${currentUserAccessFilter} && completed = false`,
sort: '-created',
fields: getTaskListFields(),
requestKey: null
@@ -2228,14 +2323,8 @@ export const initStore = async () => {
fields: getTaskListFields(),
requestKey: null
}).catch(() => []) : Promise.resolve([]),
(collaborativeKeys.length > 0) ? pb.collection(TASGRID_COLLECTION).getFullList({
filter: `(${collaborativeKeys.map(tag => `tags ~ "${tag}"`).join(' || ')}) && completed = false`,
sort: '-created',
fields: getTaskListFields(),
requestKey: null
}).catch(() => []) : Promise.resolve([]),
(supervisedUserIds.length > 0) ? pb.collection(TASGRID_COLLECTION).getFullList({
filter: `(${supervisedUserIds.map(id => relationFilter("user", id)).join(' || ')}) && completed = false`,
filter: `${supervisedAccessFilter} && completed = false`,
sort: '-created',
fields: getTaskListFields(),
requestKey: null
@@ -2302,8 +2391,9 @@ export const initStore = async () => {
if (remainingSlots > 0) {
try {
const completedAccessFilter = buildPersonalAccessFilter(currentUserId ? [currentUserId] : []);
const recentCompleted = await pb.collection(TASGRID_COLLECTION).getList(1, remainingSlots, {
filter: `${relationFilter("user", currentUserId)} && completed = true`,
filter: `${completedAccessFilter} && completed = true`,
sort: '-updated',
fields: getTaskListFields(),
requestKey: null
@@ -2346,7 +2436,7 @@ export const initStore = async () => {
// Run one-time migration automatically only in dev data mode.
if (TASGRID_IS_DEV_DATA) {
await runLegacyTagMigration();
await runPersonalContextMigration();
await runContextOwnershipMigration();
}
// Separate Templates load (less critical)
@@ -2622,7 +2712,13 @@ export const addTask = async (title: string, options?: { dueDate?: Date | string
// Apply Handoff Logic (e.g. transfer ownership if tagging for another user)
// We treat the entire new task as "updates" to check against itself
const handoffUpdates = checkForHandoffs(initialTask, initialTask);
const newTask = { ...initialTask, ...handoffUpdates };
const mirroredUpdates = withMirroredOwnerId(
initialTask.ownerId,
handoffUpdates.shareRefs || initialTask.shareRefs,
handoffUpdates,
handoffUpdates.ownerId || initialTask.ownerId
);
const newTask = { ...initialTask, ...mirroredUpdates };
// Optimistic UI update
setStore("tasks", (t) => [newTask, ...t]);
@@ -2710,7 +2806,7 @@ export const copyTask = async (id: string) => {
const checkForHandoffs = (task: Task, updates: Partial<Task>): Partial<Task> => {
const currentUserId = pb.authStore.model?.id;
if (!currentUserId || task.ownerId !== currentUserId) return updates;
if (!currentUserId || !taskHasPersonalContextAccess(task, currentUserId)) return updates;
const refs = updates.shareRefs || task.shareRefs;
const senderPersonalContext = getPersonalContext(currentUserId);
@@ -2724,6 +2820,12 @@ const checkForHandoffs = (task: Task, updates: Partial<Task>): Partial<Task> =>
for (const ref of refs) {
const context = getContextByRef(ref);
if (senderPersonalContext && ref.kind === "user" && (
ref.contextId === senderPersonalContext.id ||
ref.key === senderPersonalContext.key
)) {
continue;
}
if (!context || getTaskSharePolicy(ref) !== "handoff") continue;
const nextShareRefs = stripSenderPersonalContext(refs);
const labelTags = updates.labelTags || task.labelTags;
@@ -2801,6 +2903,13 @@ export const updateTask = async (id: string, updates: Partial<Task>) => {
);
}
finalUpdates = checkForHandoffs(currentTask, finalUpdates);
const mirroredShareRefs = finalUpdates.shareRefs || currentTask.shareRefs;
finalUpdates = withMirroredOwnerId(
finalUpdates.ownerId ?? currentTask.ownerId,
mirroredShareRefs,
finalUpdates,
finalUpdates.ownerId ?? currentTask.ownerId
);
}
// Optimistic update with timestamp to prevent stale realtime events
@@ -3141,16 +3250,11 @@ export const loadAllHistory = async () => {
const bucketTags = getSubscribedBucketContexts()
.map(context => buildShareTag(context.displayName));
const supervisedUserIds = store.supervisedContexts.map(context => context.ownerUserId);
const collaborativeTags = store.contexts
.filter(context =>
!context.deletedAt &&
context.kind === "user" &&
context.targetUserId === userId
)
.map(context => buildShareTag(context.displayName));
const userAccessFilter = buildPersonalAccessFilter([userId]);
const supervisedAccessFilter = buildPersonalAccessFilter(supervisedUserIds);
const promises = [
pb.collection(TASGRID_COLLECTION).getFullList({ filter: relationFilter("user", userId), sort: '-created', fields: getTaskListFields(), requestKey: null })
pb.collection(TASGRID_COLLECTION).getFullList({ filter: userAccessFilter, sort: '-created', fields: getTaskListFields(), requestKey: null })
];
if (bucketTags.length > 0) {
@@ -3162,18 +3266,9 @@ export const loadAllHistory = async () => {
}).catch(() => []));
}
if (collaborativeTags.length > 0) {
promises.push(pb.collection(TASGRID_COLLECTION).getFullList({
filter: collaborativeTags.map(tag => `tags ~ "${tag}"`).join(' || '),
sort: '-created',
fields: getTaskListFields(),
requestKey: null
}).catch(() => []));
}
if (supervisedUserIds.length > 0) {
promises.push(pb.collection(TASGRID_COLLECTION).getFullList({
filter: supervisedUserIds.map(id => relationFilter("user", id)).join(' || '),
filter: supervisedAccessFilter,
sort: '-created',
fields: getTaskListFields(),
requestKey: null
@@ -3406,10 +3501,7 @@ export const loadTasksForOwner = async (userId: string) => {
if (!pb.authStore.isValid || !userId) return;
try {
const targetPersonalContext = getPersonalContext(userId);
const filter = targetPersonalContext
? `tags ~ "${buildShareTag(targetPersonalContext.displayName)}"`
: relationFilter("user", userId);
const filter = buildPersonalAccessFilter([userId]);
const records = await pb.collection(TASGRID_COLLECTION).getFullList({
filter,
sort: '-created',