reliability updates (test)
This commit is contained in:
+10
-2
@@ -115,8 +115,16 @@ const App: Component = () => {
|
|||||||
<Show when={getEmbedView() === 'embed_notes'}>
|
<Show when={getEmbedView() === 'embed_notes'}>
|
||||||
{(() => {
|
{(() => {
|
||||||
const noteIdFromUrl = createMemo(() => {
|
const noteIdFromUrl = createMemo(() => {
|
||||||
const params = new URLSearchParams(new URL(location()).search);
|
const loc = location();
|
||||||
return params.get('noteId');
|
const url = new URL(loc);
|
||||||
|
// Try standard search params
|
||||||
|
let id = url.searchParams.get('noteId');
|
||||||
|
if (!id && url.hash.includes('?')) {
|
||||||
|
// Try parsing from hash (for hash-based routing)
|
||||||
|
const hashSearch = url.hash.split('?')[1];
|
||||||
|
id = new URLSearchParams(hashSearch).get('noteId');
|
||||||
|
}
|
||||||
|
return id;
|
||||||
});
|
});
|
||||||
console.log('[DEBUG App] Rendering embed/notes context. noteId trace:', noteIdFromUrl());
|
console.log('[DEBUG App] Rendering embed/notes context. noteId trace:', noteIdFromUrl());
|
||||||
return <NotepadView selectedNoteId={noteIdFromUrl()} setSelectedNoteId={() => { }} hideNavigation={!!noteIdFromUrl()} />;
|
return <NotepadView selectedNoteId={noteIdFromUrl()} setSelectedNoteId={() => { }} hideNavigation={!!noteIdFromUrl()} />;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import PocketBase from 'pocketbase';
|
import PocketBase from 'pocketbase';
|
||||||
|
|
||||||
export const pb = new PocketBase('https://pocketbase.ccllc.pro');
|
export const pb = new PocketBase('https://pocketbase.ccllc.pro');
|
||||||
|
pb.autoCancellation(false);
|
||||||
|
|
||||||
export const TASGRID_COLLECTION = 'TasGrid';
|
export const TASGRID_COLLECTION = 'TasGrid';
|
||||||
export const TAGS_COLLECTION = 'TasGrid_Tags';
|
export const TAGS_COLLECTION = 'TasGrid_Tags';
|
||||||
|
|||||||
+304
-297
@@ -756,340 +756,347 @@ export const subscribeToRealtime = async () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let isInitializing = false;
|
||||||
export const initStore = async () => {
|
export const initStore = async () => {
|
||||||
|
if (isInitializing) return;
|
||||||
if (!pb.authStore.isValid) return;
|
if (!pb.authStore.isValid) return;
|
||||||
|
|
||||||
// 0. Synchronous hydration from localStorage for "instant" first paint
|
isInitializing = true;
|
||||||
const key = getStorageKey();
|
|
||||||
if (key) {
|
|
||||||
const saved = localStorage.getItem(key);
|
|
||||||
if (saved) {
|
|
||||||
try {
|
|
||||||
setStore(JSON.parse(saved));
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Failed to parse cached data", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Heartbeat - Ensure only one interval is running
|
|
||||||
if (heartbeatInterval) clearInterval(heartbeatInterval);
|
|
||||||
heartbeatInterval = setInterval(() => {
|
|
||||||
setNow(Date.now());
|
|
||||||
checkRecurringTasks(); // Check for recurrence triggers (minutely, etc.)
|
|
||||||
}, 60000);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 1. Fetch Preferences from User Profile (Weights, Scale, Tags)
|
// 0. Synchronous hydration from localStorage for "instant" first paint
|
||||||
try {
|
const key = getStorageKey();
|
||||||
const userId = pb.authStore.model?.id;
|
if (key) {
|
||||||
if (userId) {
|
const saved = localStorage.getItem(key);
|
||||||
const user = await pb.collection('users').getOne(userId, { requestKey: null });
|
if (saved) {
|
||||||
const prefs = user.Taskgrid_pref || {};
|
try {
|
||||||
|
setStore(JSON.parse(saved));
|
||||||
setStore({
|
} catch (e) {
|
||||||
pWeight: prefs.pWeight || 1.0,
|
console.error("Failed to parse cached data", e);
|
||||||
uWeight: prefs.uWeight || 1.0,
|
}
|
||||||
matrixScaleDays: prefs.matrixScaleDays || 30,
|
|
||||||
prefId: userId,
|
|
||||||
subscribedBuckets: user.subscribedBuckets || []
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
} catch (prefErr) {
|
|
||||||
console.warn("Failed to load preferences:", prefErr);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1.2 Fetch Buckets
|
// Heartbeat - Ensure only one interval is running
|
||||||
try {
|
if (heartbeatInterval) clearInterval(heartbeatInterval);
|
||||||
const buckets = await pb.collection(BUCKETS_COLLECTION).getFullList({ sort: 'name', requestKey: null });
|
heartbeatInterval = setInterval(() => {
|
||||||
setStore("buckets", buckets.map((b: any) => ({
|
setNow(Date.now());
|
||||||
id: b.id,
|
checkRecurringTasks(); // Check for recurrence triggers (minutely, etc.)
|
||||||
name: b.name,
|
}, 60000);
|
||||||
color: b.color,
|
|
||||||
description: b.description
|
|
||||||
})));
|
|
||||||
} catch (bucketErr) {
|
|
||||||
console.warn("Failed to load buckets (collection might not exist yet):", bucketErr);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1.3 Fetch Notes
|
|
||||||
try {
|
try {
|
||||||
|
// 1. Fetch Preferences from User Profile (Weights, Scale, Tags)
|
||||||
|
try {
|
||||||
|
const userId = pb.authStore.model?.id;
|
||||||
|
if (userId) {
|
||||||
|
const user = await pb.collection('users').getOne(userId, { requestKey: null });
|
||||||
|
const prefs = user.Taskgrid_pref || {};
|
||||||
|
|
||||||
|
setStore({
|
||||||
|
pWeight: prefs.pWeight || 1.0,
|
||||||
|
uWeight: prefs.uWeight || 1.0,
|
||||||
|
matrixScaleDays: prefs.matrixScaleDays || 30,
|
||||||
|
prefId: userId,
|
||||||
|
subscribedBuckets: user.subscribedBuckets || []
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (prefErr) {
|
||||||
|
console.warn("Failed to load preferences:", prefErr);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1.2 Fetch Buckets
|
||||||
|
try {
|
||||||
|
const buckets = await pb.collection(BUCKETS_COLLECTION).getFullList({ sort: 'name', requestKey: null });
|
||||||
|
setStore("buckets", buckets.map((b: any) => ({
|
||||||
|
id: b.id,
|
||||||
|
name: b.name,
|
||||||
|
color: b.color,
|
||||||
|
description: b.description
|
||||||
|
})));
|
||||||
|
} catch (bucketErr) {
|
||||||
|
console.warn("Failed to load buckets (collection might not exist yet):", bucketErr);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1.3 Fetch Notes
|
||||||
|
try {
|
||||||
|
const currentUserId = pb.authStore.model?.id;
|
||||||
|
const notesRecords = await pb.collection(NOTES_COLLECTION).getFullList({
|
||||||
|
filter: `isPrivate = false || user = "${currentUserId}"`,
|
||||||
|
sort: '-created',
|
||||||
|
requestKey: null
|
||||||
|
});
|
||||||
|
setStore("notes", notesRecords.map(mapRecordToNote));
|
||||||
|
} catch (notesErr) {
|
||||||
|
console.warn("Failed to load notes (collection might not exist yet):", notesErr);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1.5. Fetch Tag Definitions
|
||||||
|
try {
|
||||||
|
const tagRecords = await pb.collection(TAGS_COLLECTION).getFullList({
|
||||||
|
filter: `user = "${pb.authStore.model?.id}"`,
|
||||||
|
requestKey: null
|
||||||
|
});
|
||||||
|
const loadedTags: TagDefinition[] = tagRecords.map(r => ({
|
||||||
|
id: r.id,
|
||||||
|
name: r.name,
|
||||||
|
value: r.value,
|
||||||
|
color: r.color,
|
||||||
|
theme: r.theme,
|
||||||
|
isUser: r.isUser || false,
|
||||||
|
isBucket: r.isBucket || false
|
||||||
|
}));
|
||||||
|
setStore("tagDefinitions", reconcile(loadedTags));
|
||||||
|
} catch (tagErr) {
|
||||||
|
console.warn("Failed to load tags:", tagErr);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1.8 Fetch Share Rules (CRITICAL for filter construction)
|
||||||
|
// We need these loaded BEFORE we construct the focus filter
|
||||||
|
await loadShareRules();
|
||||||
|
|
||||||
const currentUserId = pb.authStore.model?.id;
|
const currentUserId = pb.authStore.model?.id;
|
||||||
const notesRecords = await pb.collection(NOTES_COLLECTION).getFullList({
|
if (!currentUserId) return;
|
||||||
filter: `isPrivate = false || user = "${currentUserId}"`,
|
|
||||||
sort: '-created',
|
|
||||||
requestKey: null
|
|
||||||
});
|
|
||||||
setStore("notes", notesRecords.map(mapRecordToNote));
|
|
||||||
} catch (notesErr) {
|
|
||||||
console.warn("Failed to load notes (collection might not exist yet):", notesErr);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1.5. Fetch Tag Definitions
|
// --- PHASE 2: Multi-Stage Loading ---
|
||||||
try {
|
|
||||||
const tagRecords = await pb.collection(TAGS_COLLECTION).getFullList({
|
|
||||||
filter: `user = "${pb.authStore.model?.id}"`,
|
|
||||||
requestKey: null
|
|
||||||
});
|
|
||||||
const loadedTags: TagDefinition[] = tagRecords.map(r => ({
|
|
||||||
id: r.id,
|
|
||||||
name: r.name,
|
|
||||||
value: r.value,
|
|
||||||
color: r.color,
|
|
||||||
theme: r.theme,
|
|
||||||
isUser: r.isUser || false,
|
|
||||||
isBucket: r.isBucket || false
|
|
||||||
}));
|
|
||||||
setStore("tagDefinitions", reconcile(loadedTags));
|
|
||||||
} catch (tagErr) {
|
|
||||||
console.warn("Failed to load tags:", tagErr);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1.8 Fetch Share Rules (CRITICAL for filter construction)
|
// --- WINDOWED SYNC STRATEGY ---
|
||||||
// We need these loaded BEFORE we construct the focus filter
|
|
||||||
await loadShareRules();
|
|
||||||
|
|
||||||
const currentUserId = pb.authStore.model?.id;
|
// STAGE 1: Instant Focus (Top 30 Incomplete - User Only)
|
||||||
if (!currentUserId) return;
|
// Goal: < 100ms First Paint. Uses 'idx_user_focused'.
|
||||||
|
try {
|
||||||
// --- PHASE 2: Multi-Stage Loading ---
|
const focusRecords = await pb.collection(TASGRID_COLLECTION).getList(1, 30, {
|
||||||
|
filter: `user = "${currentUserId}" && completed = false && deletedAt = ""`,
|
||||||
// --- WINDOWED SYNC STRATEGY ---
|
sort: '-priority,dueDate',
|
||||||
|
requestKey: 'initial_focus'
|
||||||
// STAGE 1: Instant Focus (Top 30 Incomplete - User Only)
|
|
||||||
// Goal: < 100ms First Paint. Uses 'idx_user_focused'.
|
|
||||||
try {
|
|
||||||
const focusRecords = await pb.collection(TASGRID_COLLECTION).getList(1, 30, {
|
|
||||||
filter: `user = "${currentUserId}" && completed = false && deletedAt = ""`,
|
|
||||||
sort: '-priority,dueDate',
|
|
||||||
requestKey: 'initial_focus'
|
|
||||||
});
|
|
||||||
|
|
||||||
// Filter out templates client-side to ensure tasks with empty/null tags aren't excluded by DB
|
|
||||||
const focusTasks = focusRecords.items
|
|
||||||
.map(mapRecordToTask)
|
|
||||||
.filter(t => !t.tags?.includes("__template__"));
|
|
||||||
|
|
||||||
setStore("tasks", focusTasks);
|
|
||||||
} catch (focusErr) {
|
|
||||||
console.warn("Stage 1 load failed:", focusErr);
|
|
||||||
}
|
|
||||||
|
|
||||||
// STAGE 2 & 3: Background Sync
|
|
||||||
const backgroundSync = async () => {
|
|
||||||
|
|
||||||
const bucketNames = store.subscribedBuckets
|
|
||||||
.map(id => store.buckets.find(b => b.id === id)?.name)
|
|
||||||
.filter(Boolean);
|
|
||||||
|
|
||||||
// Sync System Tags & Share Rules FIRST to ensure Self-Rules exist for query construction
|
|
||||||
await syncSystemTagsAndRules();
|
|
||||||
|
|
||||||
// Add rules from shareRules store (which was just synced)
|
|
||||||
const incomingRules = store.shareRules.filter(r => r.targetUserId === currentUserId);
|
|
||||||
|
|
||||||
// 2a. Fetch ALL Incomplete Tasks (Metadata Only) to ensure availability
|
|
||||||
// We must replicate the complex sharing logic here
|
|
||||||
const incompletePromises = [
|
|
||||||
// Own Incomplete
|
|
||||||
pb.collection(TASGRID_COLLECTION).getFullList({
|
|
||||||
filter: `user = "${currentUserId}" && completed = false`,
|
|
||||||
sort: '-created',
|
|
||||||
fields: LIST_FIELDS,
|
|
||||||
requestKey: null
|
|
||||||
}),
|
|
||||||
// Shared With Individual Incomplete
|
|
||||||
pb.collection(TASGRID_COLLECTION).getFullList({
|
|
||||||
filter: `sharedWith ~ "${currentUserId}" && completed = false`,
|
|
||||||
sort: '-created',
|
|
||||||
fields: LIST_FIELDS,
|
|
||||||
requestKey: null
|
|
||||||
}).catch(() => []),
|
|
||||||
// Bucket Incomplete
|
|
||||||
(bucketNames.length > 0) ? pb.collection(TASGRID_COLLECTION).getFullList({
|
|
||||||
filter: `(${bucketNames.map(name => `tags ~ "${name}"`).join(' || ')}) && completed = false`,
|
|
||||||
sort: '-created',
|
|
||||||
fields: LIST_FIELDS,
|
|
||||||
requestKey: null
|
|
||||||
}).catch(() => []) : Promise.resolve([])
|
|
||||||
];
|
|
||||||
|
|
||||||
// Share Rules Incomplete
|
|
||||||
if (incomingRules.length > 0) {
|
|
||||||
// Separate "Global Rules" (Self-Rules) from "Specific User Rules"
|
|
||||||
const globalTagRules: string[] = [];
|
|
||||||
const specificUserRules = new Map<string, { type: 'all' | 'tag', tagName?: string }[]>();
|
|
||||||
|
|
||||||
incomingRules.forEach((rule: any) => {
|
|
||||||
if (rule.ownerId === rule.targetUserId && rule.type === 'tag' && rule.tagName) {
|
|
||||||
globalTagRules.push(rule.tagName);
|
|
||||||
} else {
|
|
||||||
const existing = specificUserRules.get(rule.ownerId) || [];
|
|
||||||
existing.push({ type: rule.type, tagName: rule.tagName });
|
|
||||||
specificUserRules.set(rule.ownerId, existing);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 1. Fetch Global Rule Tasks (from ANY user)
|
// Filter out templates client-side to ensure tasks with empty/null tags aren't excluded by DB
|
||||||
// 1. Fetch Global Rule Tasks (from ANY user)
|
const focusTasks = focusRecords.items
|
||||||
if (globalTagRules.length > 0) {
|
.map(mapRecordToTask)
|
||||||
// We want tasks that have ANY of these tags, AND are not completed
|
.filter(t => !t.tags?.includes("__template__"));
|
||||||
// filter: (tags ~ "A" || tags ~ "B") && completed = false
|
|
||||||
const tagFilter = globalTagRules.map(t => `tags ~ "${t}"`).join(' || ');
|
setStore("tasks", focusTasks);
|
||||||
incompletePromises.push(pb.collection(TASGRID_COLLECTION).getFullList({
|
} catch (focusErr) {
|
||||||
filter: `(${tagFilter}) && completed = false`,
|
console.warn("Stage 1 load failed:", focusErr);
|
||||||
|
}
|
||||||
|
|
||||||
|
// STAGE 2 & 3: Background Sync
|
||||||
|
const backgroundSync = async () => {
|
||||||
|
|
||||||
|
const bucketNames = store.subscribedBuckets
|
||||||
|
.map(id => store.buckets.find(b => b.id === id)?.name)
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
// Sync System Tags & Share Rules FIRST to ensure Self-Rules exist for query construction
|
||||||
|
await syncSystemTagsAndRules();
|
||||||
|
|
||||||
|
// Add rules from shareRules store (which was just synced)
|
||||||
|
const incomingRules = store.shareRules.filter(r => r.targetUserId === currentUserId);
|
||||||
|
|
||||||
|
// 2a. Fetch ALL Incomplete Tasks (Metadata Only) to ensure availability
|
||||||
|
// We must replicate the complex sharing logic here
|
||||||
|
const incompletePromises = [
|
||||||
|
// Own Incomplete
|
||||||
|
pb.collection(TASGRID_COLLECTION).getFullList({
|
||||||
|
filter: `user = "${currentUserId}" && completed = false`,
|
||||||
sort: '-created',
|
sort: '-created',
|
||||||
fields: LIST_FIELDS,
|
fields: LIST_FIELDS,
|
||||||
requestKey: null
|
requestKey: null
|
||||||
}).catch(() => []));
|
}),
|
||||||
|
// Shared With Individual Incomplete
|
||||||
|
pb.collection(TASGRID_COLLECTION).getFullList({
|
||||||
|
filter: `sharedWith ~ "${currentUserId}" && completed = false`,
|
||||||
|
sort: '-created',
|
||||||
|
fields: LIST_FIELDS,
|
||||||
|
requestKey: null
|
||||||
|
}).catch(() => []),
|
||||||
|
// Bucket Incomplete
|
||||||
|
(bucketNames.length > 0) ? pb.collection(TASGRID_COLLECTION).getFullList({
|
||||||
|
filter: `(${bucketNames.map(name => `tags ~ "${name}"`).join(' || ')}) && completed = false`,
|
||||||
|
sort: '-created',
|
||||||
|
fields: LIST_FIELDS,
|
||||||
|
requestKey: null
|
||||||
|
}).catch(() => []) : Promise.resolve([])
|
||||||
|
];
|
||||||
|
|
||||||
|
// Share Rules Incomplete
|
||||||
|
if (incomingRules.length > 0) {
|
||||||
|
// Separate "Global Rules" (Self-Rules) from "Specific User Rules"
|
||||||
|
const globalTagRules: string[] = [];
|
||||||
|
const specificUserRules = new Map<string, { type: 'all' | 'tag', tagName?: string }[]>();
|
||||||
|
|
||||||
|
incomingRules.forEach((rule: any) => {
|
||||||
|
if (rule.ownerId === rule.targetUserId && rule.type === 'tag' && rule.tagName) {
|
||||||
|
globalTagRules.push(rule.tagName);
|
||||||
|
} else {
|
||||||
|
const existing = specificUserRules.get(rule.ownerId) || [];
|
||||||
|
existing.push({ type: rule.type, tagName: rule.tagName });
|
||||||
|
specificUserRules.set(rule.ownerId, existing);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 1. Fetch Global Rule Tasks (from ANY user)
|
||||||
|
// 1. Fetch Global Rule Tasks (from ANY user)
|
||||||
|
if (globalTagRules.length > 0) {
|
||||||
|
// We want tasks that have ANY of these tags, AND are not completed
|
||||||
|
// filter: (tags ~ "A" || tags ~ "B") && completed = false
|
||||||
|
const tagFilter = globalTagRules.map(t => `tags ~ "${t}"`).join(' || ');
|
||||||
|
incompletePromises.push(pb.collection(TASGRID_COLLECTION).getFullList({
|
||||||
|
filter: `(${tagFilter}) && completed = false`,
|
||||||
|
sort: '-created',
|
||||||
|
fields: LIST_FIELDS,
|
||||||
|
requestKey: null
|
||||||
|
}).catch(() => []));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Fetch Specific User Rule Tasks (from specific owner)
|
||||||
|
Array.from(specificUserRules.entries()).forEach(([ownerId, rules]) => {
|
||||||
|
const hasAllRule = rules.some(r => r.type === 'all');
|
||||||
|
const tagRules = rules.filter(r => r.type === 'tag').map(r => r.tagName).filter(Boolean);
|
||||||
|
|
||||||
|
let filter = `user = "${ownerId}" && completed = false`;
|
||||||
|
|
||||||
|
if (hasAllRule) {
|
||||||
|
// Get everything from this user
|
||||||
|
} else if (tagRules.length > 0) {
|
||||||
|
// Get specific tags from this user
|
||||||
|
filter += ` && (${tagRules.map(t => `tags ~ "${t}"`).join(' || ')})`;
|
||||||
|
} else {
|
||||||
|
return; // No valid rules for this user
|
||||||
|
}
|
||||||
|
|
||||||
|
incompletePromises.push(pb.collection(TASGRID_COLLECTION).getFullList({
|
||||||
|
filter,
|
||||||
|
sort: '-created',
|
||||||
|
fields: LIST_FIELDS,
|
||||||
|
requestKey: null
|
||||||
|
}).catch(() => []));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Fetch Specific User Rule Tasks (from specific owner)
|
const stage2Results = await Promise.all(incompletePromises);
|
||||||
Array.from(specificUserRules.entries()).forEach(([ownerId, rules]) => {
|
const allIncomplete = stage2Results.flat();
|
||||||
const hasAllRule = rules.some(r => r.type === 'all');
|
|
||||||
const tagRules = rules.filter(r => r.type === 'tag').map(r => r.tagName).filter(Boolean);
|
|
||||||
|
|
||||||
let filter = `user = "${ownerId}" && completed = false`;
|
setStore("tasks", (currentTasks) => {
|
||||||
|
const taskMap = new Map(currentTasks.map(t => [t.id, t]));
|
||||||
if (hasAllRule) {
|
allIncomplete.forEach((r: any) => {
|
||||||
// Get everything from this user
|
if (r.tags?.includes("__template__")) return;
|
||||||
} else if (tagRules.length > 0) {
|
const existing = taskMap.get(r.id);
|
||||||
// Get specific tags from this user
|
// If existing has content, keep it. Otherwise use new record (which likely has no content yet)
|
||||||
filter += ` && (${tagRules.map(t => `tags ~ "${t}"`).join(' || ')})`;
|
const newTask = mapRecordToTask(r);
|
||||||
} else {
|
if (existing && existing.content) {
|
||||||
return; // No valid rules for this user
|
newTask.content = existing.content;
|
||||||
}
|
}
|
||||||
|
taskMap.set(r.id, newTask);
|
||||||
incompletePromises.push(pb.collection(TASGRID_COLLECTION).getFullList({
|
});
|
||||||
filter,
|
return Array.from(taskMap.values());
|
||||||
sort: '-created',
|
|
||||||
fields: LIST_FIELDS,
|
|
||||||
requestKey: null
|
|
||||||
}).catch(() => []));
|
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
const stage2Results = await Promise.all(incompletePromises);
|
// 2b. Content Backfill for Incomplete Tasks
|
||||||
const allIncomplete = stage2Results.flat();
|
// Fetch content for tasks that don't have it yet, in chunks
|
||||||
|
const tasksNeedingContent = store.tasks.filter(t => !t.completed && t.content === undefined).map(t => t.id);
|
||||||
|
if (tasksNeedingContent.length > 0) {
|
||||||
|
// Fetch in chunks of 20 to avoid URL length limits
|
||||||
|
const chunkSize = 20;
|
||||||
|
for (let i = 0; i < tasksNeedingContent.length; i += chunkSize) {
|
||||||
|
const ids = tasksNeedingContent.slice(i, i + chunkSize);
|
||||||
|
// We fetch just ID and Content
|
||||||
|
try {
|
||||||
|
const contentRecords = await pb.collection(TASGRID_COLLECTION).getFullList({
|
||||||
|
filter: ids.map(id => `id="${id}"`).join(' || '),
|
||||||
|
fields: 'id,content',
|
||||||
|
requestKey: null
|
||||||
|
});
|
||||||
|
|
||||||
setStore("tasks", (currentTasks) => {
|
setStore("tasks", (tasks) => tasks.map(t => {
|
||||||
const taskMap = new Map(currentTasks.map(t => [t.id, t]));
|
const match = contentRecords.find((r: any) => r.id === t.id);
|
||||||
allIncomplete.forEach((r: any) => {
|
if (match) return { ...t, content: match.content };
|
||||||
if (r.tags?.includes("__template__")) return;
|
return t;
|
||||||
const existing = taskMap.get(r.id);
|
}));
|
||||||
// If existing has content, keep it. Otherwise use new record (which likely has no content yet)
|
} catch (e) { console.warn("Backfill chunk failed", e); }
|
||||||
const newTask = mapRecordToTask(r);
|
|
||||||
if (existing && existing.content) {
|
|
||||||
newTask.content = existing.content;
|
|
||||||
}
|
}
|
||||||
taskMap.set(r.id, newTask);
|
}
|
||||||
});
|
|
||||||
return Array.from(taskMap.values());
|
|
||||||
});
|
|
||||||
|
|
||||||
// 2b. Content Backfill for Incomplete Tasks
|
// STAGE 3: Recent History (Cap 150)
|
||||||
// Fetch content for tasks that don't have it yet, in chunks
|
const currentCount = store.tasks.length;
|
||||||
const tasksNeedingContent = store.tasks.filter(t => !t.completed && t.content === undefined).map(t => t.id);
|
const remainingSlots = 150 - currentCount;
|
||||||
if (tasksNeedingContent.length > 0) {
|
|
||||||
// Fetch in chunks of 20 to avoid URL length limits
|
if (remainingSlots > 0) {
|
||||||
const chunkSize = 20;
|
|
||||||
for (let i = 0; i < tasksNeedingContent.length; i += chunkSize) {
|
|
||||||
const ids = tasksNeedingContent.slice(i, i + chunkSize);
|
|
||||||
// We fetch just ID and Content
|
|
||||||
try {
|
try {
|
||||||
const contentRecords = await pb.collection(TASGRID_COLLECTION).getFullList({
|
const recentCompleted = await pb.collection(TASGRID_COLLECTION).getList(1, remainingSlots, {
|
||||||
filter: ids.map(id => `id="${id}"`).join(' || '),
|
filter: `user = "${currentUserId}" && completed = true`,
|
||||||
fields: 'id,content',
|
sort: '-updated',
|
||||||
|
fields: LIST_FIELDS,
|
||||||
requestKey: null
|
requestKey: null
|
||||||
});
|
});
|
||||||
|
|
||||||
setStore("tasks", (tasks) => tasks.map(t => {
|
setStore("tasks", (prev) => {
|
||||||
const match = contentRecords.find((r: any) => r.id === t.id);
|
const newTasks = [...prev];
|
||||||
if (match) return { ...t, content: match.content };
|
recentCompleted.items.forEach((r: any) => {
|
||||||
return t;
|
if (!newTasks.some(t => t.id === r.id)) {
|
||||||
}));
|
newTasks.push(mapRecordToTask(r));
|
||||||
} catch (e) { console.warn("Backfill chunk failed", e); }
|
}
|
||||||
}
|
});
|
||||||
}
|
return newTasks;
|
||||||
|
|
||||||
// STAGE 3: Recent History (Cap 150)
|
|
||||||
const currentCount = store.tasks.length;
|
|
||||||
const remainingSlots = 150 - currentCount;
|
|
||||||
|
|
||||||
if (remainingSlots > 0) {
|
|
||||||
try {
|
|
||||||
const recentCompleted = await pb.collection(TASGRID_COLLECTION).getList(1, remainingSlots, {
|
|
||||||
filter: `user = "${currentUserId}" && completed = true`,
|
|
||||||
sort: '-updated',
|
|
||||||
fields: LIST_FIELDS,
|
|
||||||
requestKey: null
|
|
||||||
});
|
|
||||||
|
|
||||||
setStore("tasks", (prev) => {
|
|
||||||
const newTasks = [...prev];
|
|
||||||
recentCompleted.items.forEach((r: any) => {
|
|
||||||
if (!newTasks.some(t => t.id === r.id)) {
|
|
||||||
newTasks.push(mapRecordToTask(r));
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
return newTasks;
|
} catch (e) {
|
||||||
});
|
console.warn("Stage 3 load failed", e);
|
||||||
} catch (e) {
|
}
|
||||||
console.warn("Stage 3 load failed", e);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
checkRecurringTasks();
|
checkRecurringTasks();
|
||||||
|
|
||||||
// Reconstructive migration
|
// Reconstructive migration
|
||||||
const foundTagNames = new Set<string>();
|
const foundTagNames = new Set<string>();
|
||||||
store.tasks.forEach(t => t.tags?.forEach(tag => foundTagNames.add(tag)));
|
store.tasks.forEach(t => t.tags?.forEach(tag => foundTagNames.add(tag)));
|
||||||
for (const tagName of foundTagNames) {
|
for (const tagName of foundTagNames) {
|
||||||
if (tagName === "__template__") continue;
|
if (tagName === "__template__") continue;
|
||||||
if (!store.tagDefinitions.find(d => d.name === tagName)) {
|
if (!store.tagDefinitions.find(d => d.name === tagName)) {
|
||||||
await upsertTagDefinition(tagName, 5, undefined, document.documentElement.classList.contains("dark") ? "dark" : "light");
|
await upsertTagDefinition(tagName, 5, undefined, document.documentElement.classList.contains("dark") ? "dark" : "light");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sync System Tags & Share Rules (Moved to top of backgroundSync to ensure rules exist before use)
|
||||||
|
// await syncSystemTagsAndRules();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fire off background sync after a short delay
|
||||||
|
setTimeout(backgroundSync, 100);
|
||||||
|
|
||||||
|
// Separate Templates load (less critical)
|
||||||
|
try {
|
||||||
|
const templates = await pb.collection(TASGRID_COLLECTION).getFullList({
|
||||||
|
filter: `user = "${currentUserId}" && tags ~ "__template__"`,
|
||||||
|
requestKey: 'templates_load'
|
||||||
|
});
|
||||||
|
const loadedTemplates = templates.map(r => {
|
||||||
|
let meta: any = {};
|
||||||
|
try { meta = JSON.parse(r.content || "{}"); } catch (e) { }
|
||||||
|
return {
|
||||||
|
id: r.id,
|
||||||
|
name: r.title,
|
||||||
|
title: meta.title || "",
|
||||||
|
priority: r.priority,
|
||||||
|
urgency: meta.urgency || 5,
|
||||||
|
tags: meta.tags || [],
|
||||||
|
content: meta.content || ""
|
||||||
|
};
|
||||||
|
});
|
||||||
|
setStore("templates", loadedTemplates);
|
||||||
|
} catch (err) { }
|
||||||
|
|
||||||
|
// Start real-time subscription
|
||||||
|
await subscribeToRealtime();
|
||||||
|
|
||||||
|
// 2.8 Fetch Filter Templates
|
||||||
|
await loadFilterTemplates();
|
||||||
|
} catch (err) {
|
||||||
|
if (store.tasks.length === 0) {
|
||||||
|
console.error("Failed to load data:", err);
|
||||||
|
toast.error("Failed to sync with server.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sync System Tags & Share Rules (Moved to top of backgroundSync to ensure rules exist before use)
|
|
||||||
// await syncSystemTagsAndRules();
|
|
||||||
};
|
|
||||||
|
|
||||||
// Fire off background sync after a short delay
|
|
||||||
setTimeout(backgroundSync, 100);
|
|
||||||
|
|
||||||
// Separate Templates load (less critical)
|
|
||||||
try {
|
|
||||||
const templates = await pb.collection(TASGRID_COLLECTION).getFullList({
|
|
||||||
filter: `user = "${currentUserId}" && tags ~ "__template__"`,
|
|
||||||
requestKey: 'templates_load'
|
|
||||||
});
|
|
||||||
const loadedTemplates = templates.map(r => {
|
|
||||||
let meta: any = {};
|
|
||||||
try { meta = JSON.parse(r.content || "{}"); } catch (e) { }
|
|
||||||
return {
|
|
||||||
id: r.id,
|
|
||||||
name: r.title,
|
|
||||||
title: meta.title || "",
|
|
||||||
priority: r.priority,
|
|
||||||
urgency: meta.urgency || 5,
|
|
||||||
tags: meta.tags || [],
|
|
||||||
content: meta.content || ""
|
|
||||||
};
|
|
||||||
});
|
|
||||||
setStore("templates", loadedTemplates);
|
|
||||||
} catch (err) { }
|
|
||||||
|
|
||||||
// Start real-time subscription
|
|
||||||
await subscribeToRealtime();
|
|
||||||
|
|
||||||
// 2.8 Fetch Filter Templates
|
|
||||||
await loadFilterTemplates();
|
|
||||||
} catch (err) {
|
|
||||||
if (store.tasks.length === 0) {
|
|
||||||
console.error("Failed to load data:", err);
|
|
||||||
toast.error("Failed to sync with server.");
|
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
isInitializing = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+12
-6
@@ -232,7 +232,8 @@ function syncAuth(iframe) {
|
|||||||
<span>Notes Embed</span>
|
<span>Notes Embed</span>
|
||||||
<code style="font-size: 10px;">/embed/notes</code>
|
<code style="font-size: 10px;">/embed/notes</code>
|
||||||
</div>
|
</div>
|
||||||
<iframe id="notes-iframe" src="http://localhost:4000/embed/notes" onload="syncAuthOnLoad(this)"></iframe>
|
<iframe id="notes-iframe" src="https://tasgrid.ccllc.pro/embed/notes"
|
||||||
|
onload="syncAuthOnLoad(this)"></iframe>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="iframe-container">
|
<div class="iframe-container">
|
||||||
@@ -241,7 +242,7 @@ function syncAuth(iframe) {
|
|||||||
<span>Quick Add Embed</span>
|
<span>Quick Add Embed</span>
|
||||||
<code style="font-size: 10px;">/embed/quick-add</code>
|
<code style="font-size: 10px;">/embed/quick-add</code>
|
||||||
</div>
|
</div>
|
||||||
<iframe id="quick-add-iframe" src="http://localhost:4000/embed/quick-add"
|
<iframe id="quick-add-iframe" src="https://tasgrid.ccllc.pro/embed/quick-add"
|
||||||
onload="syncAuthOnLoad(this)"></iframe>
|
onload="syncAuthOnLoad(this)"></iframe>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -253,8 +254,13 @@ function syncAuth(iframe) {
|
|||||||
function syncAuthOnLoad(iframe) {
|
function syncAuthOnLoad(iframe) {
|
||||||
console.log(`Iframe ${iframe.id} loaded, checking auth sync...`);
|
console.log(`Iframe ${iframe.id} loaded, checking auth sync...`);
|
||||||
if (pb.authStore.isValid) {
|
if (pb.authStore.isValid) {
|
||||||
console.log(`Syncing existing auth to ${iframe.id}`);
|
console.log(`Syncing existing auth to ${iframe.id} ONLY`);
|
||||||
sendAuthToIframes();
|
const payload = {
|
||||||
|
type: "TASGRID_AUTH",
|
||||||
|
token: pb.authStore.token,
|
||||||
|
user: pb.authStore.model
|
||||||
|
};
|
||||||
|
iframe.contentWindow.postMessage(payload, "*");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -262,9 +268,9 @@ function syncAuth(iframe) {
|
|||||||
const id = document.getElementById('note-id-input').value.trim();
|
const id = document.getElementById('note-id-input').value.trim();
|
||||||
const iframe = document.getElementById('notes-iframe');
|
const iframe = document.getElementById('notes-iframe');
|
||||||
if (id) {
|
if (id) {
|
||||||
iframe.src = `http://localhost:4000/embed/notes?noteId=${id}`;
|
iframe.src = `https://tasgrid.ccllc.pro/embed/notes?noteId=${id}`;
|
||||||
} else {
|
} else {
|
||||||
iframe.src = `http://localhost:4000/embed/notes`;
|
iframe.src = `https://tasgrid.ccllc.pro/embed/notes`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user