debug(reading): add comprehensive logging for position restore and save

Add detailed console logs to trace:
- Position collection and stabilization
- Save scheduling, suppression, and execution
- Restore calculations and decisions
- Scroll deltas and thresholds

Logs use [reading-position] prefix with emoji indicators for easy filtering and visual scanning.
This commit is contained in:
Gigi
2025-10-22 23:14:29 +02:00
parent d650997ff9
commit ea69740fc8
3 changed files with 62 additions and 6 deletions

View File

@@ -162,20 +162,24 @@ const ContentPanel: React.FC<ContentPanelProps> = ({
// Callback to save reading position // Callback to save reading position
const handleSavePosition = useCallback(async (position: number) => { const handleSavePosition = useCallback(async (position: number) => {
if (!activeAccount || !relayPool || !eventStore || !articleIdentifier) { if (!activeAccount || !relayPool || !eventStore || !articleIdentifier) {
console.log('[reading-position] ❌ Cannot save: missing dependencies')
return return
} }
if (!settings?.syncReadingPosition) { if (!settings?.syncReadingPosition) {
console.log('[reading-position] ⚠️ Save skipped: sync disabled in settings')
return return
} }
// Check if content is long enough to track reading progress // Check if content is long enough to track reading progress
if (!shouldTrackReadingProgress(html, markdown)) { if (!shouldTrackReadingProgress(html, markdown)) {
console.log('[reading-position] ⚠️ Save skipped: content too short')
return return
} }
const scrollTop = window.pageYOffset || document.documentElement.scrollTop const scrollTop = window.pageYOffset || document.documentElement.scrollTop
try { try {
console.log('[reading-position] 🚀 Publishing position', Math.round(position * 100) + '% to relays...')
const factory = new EventFactory({ signer: activeAccount }) const factory = new EventFactory({ signer: activeAccount })
await saveReadingPosition( await saveReadingPosition(
relayPool, relayPool,
@@ -188,8 +192,9 @@ const ContentPanel: React.FC<ContentPanelProps> = ({
scrollTop scrollTop
} }
) )
console.log('[reading-position] ✅ Position published successfully')
} catch (error) { } catch (error) {
console.error('[progress] ❌ ContentPanel: Failed to save reading position:', error) console.error('[reading-position] ❌ Failed to save reading position:', error)
} }
}, [activeAccount, relayPool, eventStore, articleIdentifier, settings?.syncReadingPosition, html, markdown]) }, [activeAccount, relayPool, eventStore, articleIdentifier, settings?.syncReadingPosition, html, markdown])
@@ -217,12 +222,15 @@ const ContentPanel: React.FC<ContentPanelProps> = ({
// Load saved reading position when article loads (stabilized one-shot restore) // Load saved reading position when article loads (stabilized one-shot restore)
useEffect(() => { useEffect(() => {
if (!isTextContent || !activeAccount || !relayPool || !eventStore || !articleIdentifier) { if (!isTextContent || !activeAccount || !relayPool || !eventStore || !articleIdentifier) {
console.log('[reading-position] ⏭️ Restore skipped: missing dependencies or not text content')
return return
} }
if (settings?.syncReadingPosition === false) { if (settings?.syncReadingPosition === false) {
console.log('[reading-position] ⏭️ Restore skipped: sync disabled in settings')
return return
} }
console.log('[reading-position] 🔄 Initiating restore for article:', articleIdentifier)
const collector = collectReadingPositionsOnce({ const collector = collectReadingPositionsOnce({
relayPool, relayPool,
eventStore, eventStore,
@@ -232,7 +240,12 @@ const ContentPanel: React.FC<ContentPanelProps> = ({
}) })
collector.onStable((bestPosition) => { collector.onStable((bestPosition) => {
if (!bestPosition) return if (!bestPosition) {
console.log('[reading-position] No position to restore')
return
}
console.log('[reading-position] 🎯 Stable position received:', Math.round(bestPosition.position * 100) + '%')
// Wait for content to be fully rendered // Wait for content to be fully rendered
setTimeout(() => { setTimeout(() => {
@@ -242,13 +255,25 @@ const ContentPanel: React.FC<ContentPanelProps> = ({
const currentTop = window.pageYOffset || document.documentElement.scrollTop const currentTop = window.pageYOffset || document.documentElement.scrollTop
const targetTop = bestPosition.position * maxScroll const targetTop = bestPosition.position * maxScroll
console.log('[reading-position] 📐 Restore calculation:', {
docHeight: docH,
winHeight: winH,
maxScroll,
currentTop,
targetTop,
targetPercent: Math.round(bestPosition.position * 100) + '%'
})
// Skip if delta is too small (< 48px or < 5%) // Skip if delta is too small (< 48px or < 5%)
const deltaPx = Math.abs(targetTop - currentTop) const deltaPx = Math.abs(targetTop - currentTop)
const deltaPct = maxScroll > 0 ? Math.abs((targetTop - currentTop) / maxScroll) : 0 const deltaPct = maxScroll > 0 ? Math.abs((targetTop - currentTop) / maxScroll) : 0
if (deltaPx < 48 || deltaPct < 0.05) { if (deltaPx < 48 || deltaPct < 0.05) {
console.log('[reading-position] ⏭️ Restore skipped: delta too small (', deltaPx, 'px,', Math.round(deltaPct * 100) + '%)')
return return
} }
console.log('[reading-position] 📜 Restoring scroll position (delta:', deltaPx, 'px,', Math.round(deltaPct * 100) + '%)')
// Suppress saves briefly to avoid feedback loop // Suppress saves briefly to avoid feedback loop
if (suppressSavesFor) { if (suppressSavesFor) {
suppressSavesFor(1500) suppressSavesFor(1500)
@@ -259,10 +284,14 @@ const ContentPanel: React.FC<ContentPanelProps> = ({
top: targetTop, top: targetTop,
behavior: 'auto' behavior: 'auto'
}) })
console.log('[reading-position] ✅ Scroll restored to', Math.round(bestPosition.position * 100) + '%')
}, 500) // Give content time to render }, 500) // Give content time to render
}) })
return () => collector.stop() return () => {
console.log('[reading-position] 🛑 Stopping restore collector')
collector.stop()
}
}, [isTextContent, activeAccount, relayPool, eventStore, articleIdentifier, settings?.syncReadingPosition, selectedUrl, suppressSavesFor]) }, [isTextContent, activeAccount, relayPool, eventStore, articleIdentifier, settings?.syncReadingPosition, selectedUrl, suppressSavesFor])
// Save position before unmounting or changing article // Save position before unmounting or changing article

View File

@@ -34,7 +34,9 @@ export const useReadingPosition = ({
// Suppress auto-saves for a given duration (used after programmatic restore) // Suppress auto-saves for a given duration (used after programmatic restore)
const suppressSavesFor = useCallback((ms: number) => { const suppressSavesFor = useCallback((ms: number) => {
suppressUntilRef.current = Date.now() + ms const until = Date.now() + ms
suppressUntilRef.current = until
console.log('[reading-position] 🛡️ Suppressing saves for', ms, 'ms until', new Date(until).toISOString())
}, []) }, [])
// Debounced save function // Debounced save function
@@ -49,6 +51,7 @@ export const useReadingPosition = ({
clearTimeout(saveTimerRef.current) clearTimeout(saveTimerRef.current)
saveTimerRef.current = null saveTimerRef.current = null
} }
console.log('[reading-position] 💾 Instant save at 100% completion')
lastSavedPosition.current = 1 lastSavedPosition.current = 1
hasSavedOnce.current = true hasSavedOnce.current = true
lastSavedAtRef.current = Date.now() lastSavedAtRef.current = Date.now()
@@ -80,7 +83,9 @@ export const useReadingPosition = ({
} }
const remaining = Math.max(0, MIN_INTERVAL_MS - (nowMs - lastSavedAtRef.current)) const remaining = Math.max(0, MIN_INTERVAL_MS - (nowMs - lastSavedAtRef.current))
const delay = Math.max(autoSaveInterval, remaining) const delay = Math.max(autoSaveInterval, remaining)
console.log('[reading-position] ⏱️ Rescheduling save in', delay, 'ms (pos:', Math.round(currentPosition * 100) + '%)')
saveTimerRef.current = setTimeout(() => { saveTimerRef.current = setTimeout(() => {
console.log('[reading-position] 💾 Auto-save (rescheduled) at', Math.round(currentPosition * 100) + '%')
lastSavedPosition.current = currentPosition lastSavedPosition.current = currentPosition
hasSavedOnce.current = true hasSavedOnce.current = true
lastSavedAtRef.current = Date.now() lastSavedAtRef.current = Date.now()
@@ -96,7 +101,9 @@ export const useReadingPosition = ({
// Schedule new save using the larger of autoSaveInterval and MIN_INTERVAL_MS // Schedule new save using the larger of autoSaveInterval and MIN_INTERVAL_MS
const delay = Math.max(autoSaveInterval, MIN_INTERVAL_MS) const delay = Math.max(autoSaveInterval, MIN_INTERVAL_MS)
console.log('[reading-position] ⏱️ Scheduling save in', delay, 'ms (pos:', Math.round(currentPosition * 100) + '%)')
saveTimerRef.current = setTimeout(() => { saveTimerRef.current = setTimeout(() => {
console.log('[reading-position] 💾 Auto-save at', Math.round(currentPosition * 100) + '%')
lastSavedPosition.current = currentPosition lastSavedPosition.current = currentPosition
hasSavedOnce.current = true hasSavedOnce.current = true
lastSavedAtRef.current = Date.now() lastSavedAtRef.current = Date.now()
@@ -111,6 +118,7 @@ export const useReadingPosition = ({
clearTimeout(saveTimerRef.current) clearTimeout(saveTimerRef.current)
saveTimerRef.current = null saveTimerRef.current = null
} }
console.log('[reading-position] 💾 saveNow() called at', Math.round(position * 100) + '%')
lastSavedPosition.current = position lastSavedPosition.current = position
hasSavedOnce.current = true hasSavedOnce.current = true
lastSavedAtRef.current = Date.now() lastSavedAtRef.current = Date.now()
@@ -145,6 +153,9 @@ export const useReadingPosition = ({
// Schedule auto-save if sync is enabled (unless suppressed) // Schedule auto-save if sync is enabled (unless suppressed)
if (Date.now() >= suppressUntilRef.current) { if (Date.now() >= suppressUntilRef.current) {
scheduleSave(clampedProgress) scheduleSave(clampedProgress)
} else {
const remainingMs = suppressUntilRef.current - Date.now()
console.log('[reading-position] 🛡️ Save suppressed (', remainingMs, 'ms remaining) at', Math.round(clampedProgress * 100) + '%')
} }
// Completion detection with 2s hold at 100% // Completion detection with 2s hold at 100%

View File

@@ -203,10 +203,14 @@ export function collectReadingPositionsOnce(params: {
hasEmitted = true hasEmitted = true
if (candidates.length === 0) { if (candidates.length === 0) {
console.log('[reading-position] 📊 No candidates collected during stabilization window')
stableCallback(null) stableCallback(null)
return return
} }
console.log('[reading-position] 📊 Collected', candidates.length, 'position candidates:',
candidates.map(c => `${Math.round(c.position * 100)}% @${new Date(c.timestamp * 1000).toLocaleTimeString()}`).join(', '))
// Sort: newest first, then highest progress // Sort: newest first, then highest progress
candidates.sort((a, b) => { candidates.sort((a, b) => {
const timeDiff = b.timestamp - a.timestamp const timeDiff = b.timestamp - a.timestamp
@@ -214,10 +218,13 @@ export function collectReadingPositionsOnce(params: {
return b.position - a.position return b.position - a.position
}) })
console.log('[reading-position] ✅ Best position selected:', Math.round(candidates[0].position * 100) + '%',
'from', new Date(candidates[0].timestamp * 1000).toLocaleTimeString())
stableCallback(candidates[0]) stableCallback(candidates[0])
} }
// Start streaming and collecting // Start streaming and collecting
console.log('[reading-position] 🎯 Starting stabilized position collector (window:', windowMs, 'ms)')
streamStop = startReadingPositionStream( streamStop = startReadingPositionStream(
relayPool, relayPool,
eventStore, eventStore,
@@ -225,13 +232,22 @@ export function collectReadingPositionsOnce(params: {
articleIdentifier, articleIdentifier,
(pos) => { (pos) => {
if (hasEmitted) return if (hasEmitted) return
if (!pos) return if (!pos) {
if (pos.position <= 0.05 || pos.position >= 1) return console.log('[reading-position] 📥 Received null position')
return
}
if (pos.position <= 0.05 || pos.position >= 1) {
console.log('[reading-position] 🚫 Ignoring position', Math.round(pos.position * 100) + '% (outside 5%-100% range)')
return
}
console.log('[reading-position] 📥 Received position candidate:', Math.round(pos.position * 100) + '%',
'from', new Date(pos.timestamp * 1000).toLocaleTimeString())
candidates.push(pos) candidates.push(pos)
// Schedule one-shot emission if not already scheduled // Schedule one-shot emission if not already scheduled
if (!timer) { if (!timer) {
console.log('[reading-position] ⏰ Starting', windowMs, 'ms stabilization timer')
timer = setTimeout(() => { timer = setTimeout(() => {
emitStable() emitStable()
if (streamStop) streamStop() if (streamStop) streamStop()