quickload list implemented

This commit is contained in:
2026-03-03 18:29:42 -06:00
parent e5d579051d
commit 15f518c58f
3 changed files with 91 additions and 32 deletions
+5 -1
View File
@@ -30,12 +30,16 @@ const App: Component = () => {
window.addEventListener('popstate', handleLocChange); window.addEventListener('popstate', handleLocChange);
window.addEventListener('hashchange', handleLocChange); window.addEventListener('hashchange', handleLocChange);
let isFirstRun = true;
const removeListener = pb.authStore.onChange((token) => { const removeListener = pb.authStore.onChange((token) => {
console.log('[DEBUG] pb.authStore.onChange fired, token present:', !!token); console.log('[DEBUG] pb.authStore.onChange fired, token present:', !!token);
const wasAuthenticated = isAuthenticated();
setIsAuthenticated(!!token); setIsAuthenticated(!!token);
if (token) {
if (token && (isFirstRun || !wasAuthenticated)) {
initStore(); initStore();
} }
isFirstRun = false;
}, true); }, true);
onCleanup(() => { onCleanup(() => {
+1
View File
@@ -11,6 +11,7 @@ const Toaster = (props: ToasterProps) => {
theme={theme() as ToasterProps["theme"]} theme={theme() as ToasterProps["theme"]}
class="toaster group" class="toaster group"
position="top-center" position="top-center"
closeButton
toastOptions={{ toastOptions={{
classes: { classes: {
toast: toast:
+85 -31
View File
@@ -30,7 +30,7 @@ export interface Task {
tags: string[]; tags: string[];
ownerId: string | null; // Add ownerId to interface ownerId: string | null; // Add ownerId to interface
content?: string; // HTML content from Tiptap content?: string; // HTML content from Tiptap
deletedAt?: number; // Timestamp of soft delete deletedAt?: number | null; // Timestamp of soft delete or null if restored
created: string; // ISO string from PB created: string; // ISO string from PB
updated: string; // ISO string from PB updated: string; // ISO string from PB
recurrence?: { recurrence?: {
@@ -202,6 +202,7 @@ interface TaskStore {
subscribedBuckets: string[]; // IDs of buckets I'm subscribed to subscribedBuckets: string[]; // IDs of buckets I'm subscribed to
notes: Note[]; notes: Note[];
isNotepadMode: boolean; isNotepadMode: boolean;
quickloadTasks: string[];
} }
// Initial empty state // Initial empty state
@@ -227,7 +228,8 @@ export const [store, setStore] = createStore<TaskStore>({
buckets: [], buckets: [],
subscribedBuckets: [], subscribedBuckets: [],
notes: [], notes: [],
isNotepadMode: false isNotepadMode: false,
quickloadTasks: []
}); });
export const matchesFilter = (task: Task) => { export const matchesFilter = (task: Task) => {
@@ -336,6 +338,38 @@ createRoot(() => {
localStorage.setItem(key, JSON.stringify(store)); localStorage.setItem(key, JSON.stringify(store));
} }
}); });
let debouncedSync: number | undefined;
createEffect(() => {
if (store.isInitializing || !pb.authStore.model?.id) return;
// Calculate Top 20 Tasks
const incompleteTasks = store.tasks.filter(t =>
!t.completed &&
t.ownerId === pb.authStore.model?.id &&
!t.tags?.includes("__template__") &&
!t.deletedAt
);
const scoredTasks = incompleteTasks.map(t => ({ id: t.id, score: getCombinedScore(t) }));
scoredTasks.sort((a, b) => b.score - a.score);
const top20Ids = scoredTasks.slice(0, 20).map(t => t.id);
// Compare with current
const currentIds = store.quickloadTasks || [];
const isDifferent = top20Ids.length !== currentIds.length || top20Ids.some((id, i) => id !== currentIds[i]);
if (isDifferent) {
setStore("quickloadTasks", top20Ids);
// Debounce save by 10s
if (debouncedSync) window.clearTimeout(debouncedSync);
debouncedSync = window.setTimeout(() => {
syncPreferences();
}, 10000);
}
});
}); });
// -- Calibration -- // -- Calibration --
@@ -762,6 +796,45 @@ export const initStore = async () => {
if (!pb.authStore.isValid || store.isInitializing) return; if (!pb.authStore.isValid || store.isInitializing) return;
setStore("isInitializing", true); setStore("isInitializing", true);
const currentUserId = pb.authStore.model?.id;
// STAGE 1: Instant Focus from Local Auth Model (Top 20 + Fallback)
// Goal: < 100ms First Paint. Doesn't wait for any other network requests.
if (currentUserId) {
try {
const prefs = pb.authStore.model?.Taskgrid_pref || {};
const cachedQuickload = prefs.quickloadTasks || [];
let focusRecords;
if (cachedQuickload.length > 0) {
const idFilter = cachedQuickload.map((id: string) => `id = "${id}"`).join(' || ');
const res = await pb.collection(TASGRID_COLLECTION).getFullList({
filter: `(${idFilter}) && deletedAt = ""`,
requestKey: null
});
// Sort manually to match quickload order
res.sort((a, b) => cachedQuickload.indexOf(a.id) - cachedQuickload.indexOf(b.id));
focusRecords = { items: res };
} else {
focusRecords = await pb.collection(TASGRID_COLLECTION).getList(1, 30, {
filter: `user = "${currentUserId}" && completed = false && deletedAt = ""`,
sort: '-priority,dueDate',
requestKey: null
});
}
// 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);
}
}
// 0. Synchronous hydration from localStorage for "instant" first paint // 0. Synchronous hydration from localStorage for "instant" first paint
const key = getStorageKey(); const key = getStorageKey();
if (key) { if (key) {
@@ -795,7 +868,8 @@ export const initStore = async () => {
uWeight: prefs.uWeight || 1.0, uWeight: prefs.uWeight || 1.0,
matrixScaleDays: prefs.matrixScaleDays || 30, matrixScaleDays: prefs.matrixScaleDays || 30,
prefId: userId, prefId: userId,
subscribedBuckets: user.subscribedBuckets || [] subscribedBuckets: user.subscribedBuckets || [],
quickloadTasks: prefs.quickloadTasks || []
}); });
} }
} catch (prefErr) { } catch (prefErr) {
@@ -859,24 +933,6 @@ export const initStore = async () => {
// --- WINDOWED SYNC STRATEGY --- // --- WINDOWED SYNC STRATEGY ---
// 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: null
});
// 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 // STAGE 2 & 3: Background Sync
const backgroundSync = async () => { const backgroundSync = async () => {
@@ -1387,16 +1443,13 @@ export const updateTask = async (id: string, updates: Partial<Task>) => {
delete pbUpdates.ownerId; delete pbUpdates.ownerId;
} }
if (finalUpdates.deletedAt !== undefined) { if ("deletedAt" in finalUpdates) {
// PB expects a date string or null for date fields usually, // PB expects a date string or null for date fields.
// but if we defined deletedAt as a 'date' field in PB. // Using finalUpdates.deletedAt check to distinguish between soft-delete (timestamp) and restore (null/falsy)
// If we defined it as a number in PB? User didn't specify.
// Assuming it's a 'Date' field in PB based on previous conversation
// "deletedAt | Date | No"
if (finalUpdates.deletedAt) { if (finalUpdates.deletedAt) {
pbUpdates.deletedAt = new Date(finalUpdates.deletedAt).toISOString(); pbUpdates.deletedAt = new Date(finalUpdates.deletedAt).toISOString();
} else { } else {
pbUpdates.deletedAt = null; // restore pbUpdates.deletedAt = null; // restore task
} }
} }
@@ -1478,8 +1531,8 @@ export const removeTask = (id: string) => {
export const restoreTask = (id: string) => { export const restoreTask = (id: string) => {
// updateTask handles both optimistic update and PB sync // updateTask handles both optimistic update and PB sync
// Pass undefined for deletedAt, updateTask will convert to null for PB // Pass null for deletedAt to explicitly clear it in PB
updateTask(id, { deletedAt: undefined }); updateTask(id, { deletedAt: null });
}; };
export const deleteTaskPermanently = async (id: string) => { export const deleteTaskPermanently = async (id: string) => {
@@ -1956,7 +2009,8 @@ export const syncPreferences = async () => {
pWeight: store.pWeight, pWeight: store.pWeight,
uWeight: store.uWeight, uWeight: store.uWeight,
matrixScaleDays: store.matrixScaleDays, matrixScaleDays: store.matrixScaleDays,
tagDefinitions: store.tagDefinitions tagDefinitions: store.tagDefinitions,
quickloadTasks: store.quickloadTasks
}; };
await pb.collection('users').update(userId, { await pb.collection('users').update(userId, {