Developer Settings
@@ -377,6 +503,143 @@ const DeveloperSettings = () => {
+
+
+
+ Sync & Network Diagnostics
+ }
+ onClick={refreshSyncDiagnostics}
+ >
+ Refresh
+
+
+
+
+
+
+ Network Status
+
+ Connection:{' '}
+
+ {syncDiagnostics.isOnline ? 'Online' : 'Offline'}
+
+
+
+ Device Network:{' '}
+
+ {syncDiagnostics.isNetworkOn === false
+ ? 'Disconnected'
+ : syncDiagnostics.isNetworkOn === true
+ ? 'Connected'
+ : 'Unknown'}
+
+
+
+ Offline Since: {formatDateTime(syncDiagnostics.offlineSince)}
+
+
+ Last Network Check: {formatDateTime(syncDiagnostics.lastChecked)}
+
+
+
+
+
+
+ Sync Offset Information
+
+ Sync Cursor:{' '}
+
+ {syncDiagnostics.cursor ?? 'N/A'}
+
+
+
+ Last Sync:{' '}
+
+ {formatDateTime(syncDiagnostics.lastSync)}
+
+
+
+ Sync State:{' '}
+
+ {syncDiagnostics.syncing ? 'Syncing' : 'Idle'}
+
+
+
+ Pending Commands:{' '}
+
+ {syncDiagnostics.pendingCount}
+
+
+
+ Failed Commands:{' '}
+ 0 ? 'danger' : 'success'}
+ >
+ {syncDiagnostics.failedCount}
+
+
+ {syncDiagnostics.syncError && (
+
+ Sync Error: {syncDiagnostics.syncError}
+
+ )}
+
+
+
+
+
+ Recovery Actions
+
+ Clears local offline cache, sync cursor, and queued commands, then
+ re-syncs from the beginning.
+
+
+
+
+
+
+
+
{isNativePlatform && (
@@ -436,9 +699,7 @@ const DeveloperSettings = () => {
? new Date(scheduleTime)
: null
const now = new Date()
- const timeUntil = scheduledDate
- ? scheduledDate - now
- : null
+ const timeUntil = scheduledDate ? scheduledDate - now : null
return (
{
+
+
)
}
diff --git a/src/views/Timer/TimerDetails.jsx b/src/views/Timer/TimerDetails.jsx
index 349e15b..5cd8d9e 100644
--- a/src/views/Timer/TimerDetails.jsx
+++ b/src/views/Timer/TimerDetails.jsx
@@ -47,10 +47,14 @@ import {
} from '../../queries/TimeQueries'
import { useCircleMembers } from '../../queries/UserQueries'
import { useNotification } from '../../service/NotificationProvider'
+import { commandQueue, CommandType } from '../../utils/CommandQueue'
import { resolvePhotoURL } from '../../utils/Helpers'
import { getSafeBottom } from '../../utils/SafeAreaUtils'
import LoadingComponent from '../components/Loading'
+const isNetworkError = err =>
+ err instanceof TypeError && err.message === 'Failed to fetch'
+
const TimerDetails = () => {
const { choreId } = useParams()
const { fmt } = useLocalization()
@@ -256,7 +260,23 @@ const TimerDetails = () => {
})
refetchTimer()
},
- onError: () => {
+ onError: async error => {
+ if (isNetworkError(error)) {
+ const cmdId = await commandQueue.enqueue(
+ CommandType.START_CHORE,
+ choreId,
+ { id: choreId },
+ )
+ showSuccess({
+ title: 'Start queued',
+ message: "You're offline — start will sync when back online",
+ undoAction: async () => {
+ await commandQueue.cancel(cmdId)
+ },
+ })
+ return
+ }
+
showError({
title: 'Failed to start timer',
message: 'Please try again.',
@@ -278,7 +298,23 @@ const TimerDetails = () => {
})
refetchTimer()
},
- onError: () => {
+ onError: async error => {
+ if (isNetworkError(error)) {
+ const cmdId = await commandQueue.enqueue(
+ CommandType.PAUSE_CHORE,
+ choreId,
+ { id: choreId },
+ )
+ showSuccess({
+ title: 'Pause queued',
+ message: "You're offline — pause will sync when back online",
+ undoAction: async () => {
+ await commandQueue.cancel(cmdId)
+ },
+ })
+ return
+ }
+
showError({
title: 'Failed to pause timer',
message: 'Please try again.',
@@ -928,9 +964,7 @@ const TimerDetails = () => {
'MMM DD',
)
const startTime = fmt.time(pause.start)
- const endTime = pause.end
- ? fmt.time(pause.end)
- : null
+ const endTime = pause.end ? fmt.time(pause.end) : null
const realTimeDuration = isOngoing
? Math.max(
diff --git a/src/views/components/PendingBadge.jsx b/src/views/components/PendingBadge.jsx
index 8b659ce..aba2ace 100644
--- a/src/views/components/PendingBadge.jsx
+++ b/src/views/components/PendingBadge.jsx
@@ -20,6 +20,8 @@ const LABELS = {
update_chore: 'Update pending',
create_chore: 'Create pending',
delete_chore: 'Delete pending',
+ update_chore_history: 'Edit history pending',
+ delete_chore_history: 'Delete history pending',
reschedule_chore: 'Reschedule pending',
archive_chore: 'Archive pending',
unarchive_chore: 'Restore pending',
@@ -27,6 +29,16 @@ const LABELS = {
pause_chore: 'Pause pending',
}
+const formatCommandLabel = commandType => {
+ return (
+ LABELS[commandType] ||
+ commandType
+ ?.replace(/_/g, ' ')
+ ?.replace(/\b\w/g, letter => letter.toUpperCase()) ||
+ 'Pending action'
+ )
+}
+
function PendingBadge({ commands, size = 'sm', sx = {} }) {
const { ResponsiveModal } = useResponsiveModal()
const queryClient = useQueryClient()
@@ -45,6 +57,7 @@ function PendingBadge({ commands, size = 'sm', sx = {} }) {
const invalidatePending = async () => {
await queryClient.invalidateQueries({ queryKey: ['pendingCommands'] })
await queryClient.invalidateQueries({ queryKey: ['chores'] })
+ await queryClient.invalidateQueries({ queryKey: ['choreHistory'] })
}
const handleUndo = async (e, cmdId) => {
@@ -150,7 +163,7 @@ function PendingBadge({ commands, size = 'sm', sx = {} }) {
>