fix: implement UUID generation utility and replace crypto.randomUUID usage (to support genertaion when we are in http )in task and chore components

This commit is contained in:
Mo Tarbin
2026-07-09 14:44:05 -04:00
parent 7889c32be0
commit d207f7e2b2
5 changed files with 251 additions and 135 deletions

10
src/utils/UUID.js Normal file
View File

@@ -0,0 +1,10 @@
export function generateUUID() {
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
return crypto.randomUUID()
}
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
const r = (Math.random() * 16) | 0
const v = c === 'x' ? r : (r & 0x3) | 0x8
return v.toString(16)
})
}

View File

@@ -39,13 +39,14 @@ const LoginSettings = () => {
} }
const testConnection = async url => { const testConnection = async url => {
const testURL = url.replace(/\/+$/, '') + '/api/v1/resource'
const controller = new AbortController() const controller = new AbortController()
const timeoutId = setTimeout( const timeoutId = setTimeout(
() => controller.abort(), () => controller.abort(),
CONNECTION_TIMEOUT_MS, CONNECTION_TIMEOUT_MS,
) )
try { try {
const testURL = url.replace(/\/+$/, '') + '/api/v1/resource'
const response = await fetch(testURL, { const response = await fetch(testURL, {
method: 'GET', method: 'GET',
signal: controller.signal, signal: controller.signal,
@@ -55,18 +56,79 @@ const LoginSettings = () => {
if (response.status < 500) { if (response.status < 500) {
return { ok: true } return { ok: true }
} }
if (response.status === 503) {
return {
ok: false,
message:
'Server is starting up or temporarily unavailable (503). Try again in a moment.',
}
}
return { return {
ok: false, ok: false,
message: `Server responded with error ${response.status}. Please check your Donetick server.`, message: `Server responded with error ${response.status}. Please check your Donetick server.`,
} }
} catch (err) { } catch (err) {
clearTimeout(timeoutId) clearTimeout(timeoutId)
if (err.name === 'AbortError') { if (err.name === 'AbortError') {
return { return {
ok: false, ok: false,
message: `Connection timed out after ${CONNECTION_TIMEOUT_MS / 1000}s. Check the URL and ensure the server is running.`, message: `Connection timed out after ${CONNECTION_TIMEOUT_MS / 1000}s. The host may be unreachable or behind a firewall — check the IP/hostname and network.`,
} }
} }
// Try no-cors to distinguish CORS misconfiguration from server being down
const noCorsController = new AbortController()
const noCorsTimeout = setTimeout(() => noCorsController.abort(), 3000)
try {
const probeStart = Date.now()
const probe = await fetch(testURL, {
method: 'GET',
mode: 'no-cors',
signal: noCorsController.signal,
})
clearTimeout(noCorsTimeout)
if (probe.type === 'opaque') {
// Server responded but CORS headers blocked the real request
return {
ok: false,
message:
'Server is reachable but blocked the request (CORS). Ensure your Donetick server allows requests from this origin, or check the server CORS config.',
}
}
// Opaque is the only expected type for no-cors success; anything else is odd
void probeStart
} catch (probeErr) {
clearTimeout(noCorsTimeout)
if (probeErr.name !== 'AbortError') {
// Both normal and no-cors fetch threw immediately → port refused
const msg = probeErr.message?.toLowerCase() ?? ''
if (
msg.includes('getaddrinfo') ||
msg.includes('name not resolved') ||
msg.includes('err_name')
) {
return {
ok: false,
message:
'Hostname could not be resolved. Check the URL for typos or verify DNS.',
}
}
return {
ok: false,
message:
'Connection refused. The port may be wrong or nothing is listening — verify the URL and port (default Donetick port is 2021).',
}
}
// no-cors also timed out → server/host truly unreachable
return {
ok: false,
message:
'Unable to reach the server. Check the URL, port, and network connection.',
}
}
// Fallback (should rarely hit)
return { return {
ok: false, ok: false,
message: message:

View File

@@ -58,6 +58,7 @@ import {
UploadChoreAttachment, UploadChoreAttachment,
} from '../../utils/Fetcher' } from '../../utils/Fetcher'
import { isPlusAccount, resolvePhotoURL } from '../../utils/Helpers' import { isPlusAccount, resolvePhotoURL } from '../../utils/Helpers'
import { generateUUID } from '../../utils/UUID'
import Priorities from '../../utils/Priorities.jsx' import Priorities from '../../utils/Priorities.jsx'
import { getIconComponent } from '../../utils/ProjectIcons' import { getIconComponent } from '../../utils/ProjectIcons'
import { getSafeBottomPadding } from '../../utils/SafeAreaUtils.js' import { getSafeBottomPadding } from '../../utils/SafeAreaUtils.js'
@@ -131,7 +132,7 @@ const ChoreEdit = () => {
const [createdBy, setCreatedBy] = useState(0) const [createdBy, setCreatedBy] = useState(0)
const [errors, setErrors] = useState({}) const [errors, setErrors] = useState({})
const [attemptToSave, setAttemptToSave] = useState(false) const [attemptToSave, setAttemptToSave] = useState(false)
const [draftId] = useState(() => crypto.randomUUID()) const [draftId] = useState(() => generateUUID())
const [attachments, setAttachments] = useState([]) const [attachments, setAttachments] = useState([])
const [isUploadingAttachment, setIsUploadingAttachment] = useState(false) const [isUploadingAttachment, setIsUploadingAttachment] = useState(false)
const [addLabelModalOpen, setAddLabelModalOpen] = useState(false) const [addLabelModalOpen, setAddLabelModalOpen] = useState(false)

View File

@@ -99,12 +99,42 @@ const ChoreHistory = () => {
type: 'multi-select', type: 'multi-select',
icon: <FilterList />, icon: <FilterList />,
options: [ options: [
{ value: ChoreHistoryStatus.COMPLETED, label: 'Completed', color: 'success', icon: <Check sx={{ fontSize: 14 }} /> }, {
{ value: ChoreHistoryStatus.SKIPPED, label: 'Skipped', color: 'warning', icon: <Redo sx={{ fontSize: 14 }} /> }, value: ChoreHistoryStatus.COMPLETED,
{ value: ChoreHistoryStatus.PENDING_APPROVAL, label: 'Pending', color: 'neutral', icon: <HourglassEmpty sx={{ fontSize: 14 }} /> }, label: 'Completed',
{ value: ChoreHistoryStatus.REJECTED, label: 'Rejected', color: 'danger', icon: <ThumbDown sx={{ fontSize: 14 }} /> }, color: 'success',
{ value: 5, label: 'Missed', color: 'danger', icon: <RunningWithErrors sx={{ fontSize: 14 }} /> }, icon: <Check sx={{ fontSize: 14 }} />,
{ value: 6, label: 'Rescheduled', color: 'warning', icon: <Schedule sx={{ fontSize: 14 }} /> }, },
{
value: ChoreHistoryStatus.SKIPPED,
label: 'Skipped',
color: 'warning',
icon: <Redo sx={{ fontSize: 14 }} />,
},
{
value: ChoreHistoryStatus.PENDING_APPROVAL,
label: 'Pending',
color: 'neutral',
icon: <HourglassEmpty sx={{ fontSize: 14 }} />,
},
{
value: ChoreHistoryStatus.REJECTED,
label: 'Rejected',
color: 'danger',
icon: <ThumbDown sx={{ fontSize: 14 }} />,
},
{
value: 5,
label: 'Missed',
color: 'danger',
icon: <RunningWithErrors sx={{ fontSize: 14 }} />,
},
{
value: 6,
label: 'Rescheduled',
color: 'warning',
icon: <Schedule sx={{ fontSize: 14 }} />,
},
], ],
filterFn: (item, values) => values.includes(item.status), filterFn: (item, values) => values.includes(item.status),
}, },
@@ -143,8 +173,23 @@ const ChoreHistory = () => {
[performers], [performers],
) )
const { filteredData: filteredHistory, activeFilters, setFilter, clearAll, activeFilterCount } = const {
useFilter(choreHistory, filterDefs) filteredData: filteredHistory,
activeFilters,
setFilter,
clearAll,
activeFilterCount,
} = useFilter(choreHistory, filterDefs)
const sortedHistory = useMemo(
() =>
[...filteredHistory].sort(
(a, b) =>
new Date(b.performedAt || b.updatedAt) -
new Date(a.performedAt || a.updatedAt),
),
[filteredHistory],
)
const handleDelete = historyEntry => { const handleDelete = historyEntry => {
showConfirmation( showConfirmation(
@@ -296,7 +341,7 @@ const ChoreHistory = () => {
<Container maxWidth='md' sx={{ px: 0 }}> <Container maxWidth='md' sx={{ px: 0 }}>
{/* Enhanced Header Section */} {/* Enhanced Header Section */}
<Box sx={{ gap: 2, p: 2 }}> <Box sx={{ gap: 2, p: 2 }}>
{/* <Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 2, p: 2 }}> */} {/* <Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 2, p: 2 }}> */}
{/* Statistics Cards Grid - Compact Design */} {/* Statistics Cards Grid - Compact Design */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 3 }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 3 }}>
<History sx={{ fontSize: '1.5rem' }} /> <History sx={{ fontSize: '1.5rem' }} />
@@ -374,9 +419,8 @@ const ChoreHistory = () => {
</Box> </Box>
{/* History Section Header */} {/* History Section Header */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, p: 2 }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: 2, p: 2 }}>
<Analytics sx={{ fontSize: '1.5rem' }} /> <Analytics sx={{ fontSize: '1.5rem' }} />
<Typography <Typography
level='title-md' level='title-md'
@@ -386,17 +430,17 @@ const ChoreHistory = () => {
</Typography> </Typography>
</Box> </Box>
<Box sx={{ px: 2 }}> <Box sx={{ px: 2 }}>
<FilterBar <FilterBar
filterDefs={filterDefs} filterDefs={filterDefs}
activeFilters={activeFilters} activeFilters={activeFilters}
onSetFilter={setFilter} onSetFilter={setFilter}
onClearAll={clearAll} onClearAll={clearAll}
resultCount={filteredHistory.length} resultCount={filteredHistory.length}
totalCount={choreHistory.length} totalCount={choreHistory.length}
/> />
</Box> </Box>
{filteredHistory.length === 0 && activeFilterCount > 0 && ( {sortedHistory.length === 0 && activeFilterCount > 0 && (
<Box <Box
sx={{ sx={{
textAlign: 'center', textAlign: 'center',
@@ -420,114 +464,111 @@ const ChoreHistory = () => {
</Box> </Box>
)} )}
{filteredHistory.length > 0 && ( {sortedHistory.length > 0 && (
<Sheet <Sheet variant='plain' sx={{ borderRadius: 'sm', overflow: 'hidden' }}>
variant='plain' {/* Chore History List (Updated Style) */}
sx={{ borderRadius: 'sm', overflow: 'hidden' }}
>
{/* Chore History List (Updated Style) */}
<SwipeableList type={ListType.IOS} fullSwipe={false}> <SwipeableList type={ListType.IOS} fullSwipe={false}>
{filteredHistory.map((historyEntry, index) => ( {sortedHistory.map((historyEntry, index) => (
<SwipeableListItem <SwipeableListItem
key={historyEntry.id || index} key={historyEntry.id || index}
swipeActionOpen={ swipeActionOpen={
showMoreInfoId === (historyEntry.id || index) showMoreInfoId === (historyEntry.id || index)
? 'trailing' ? 'trailing'
: null : null
} }
trailingActions={ trailingActions={
<TrailingActions> <TrailingActions>
<Box <Box
sx={{ sx={{
display: 'flex', display: 'flex',
boxShadow: 'inset 2px 0 4px rgba(0,0,0,0.06)', boxShadow: 'inset 2px 0 4px rgba(0,0,0,0.06)',
zIndex: 0, zIndex: 0,
}} }}
> >
<SwipeAction onClick={() => handleEdit(historyEntry)}> <SwipeAction onClick={() => handleEdit(historyEntry)}>
<Box <Box
sx={{ sx={{
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
bgcolor: 'neutral.softBg', bgcolor: 'neutral.softBg',
color: 'neutral.700', color: 'neutral.700',
px: 3, px: 3,
height: '100%', height: '100%',
width: '100%', width: '100%',
}} }}
> >
<EditIcon sx={{ fontSize: 20 }} /> <EditIcon sx={{ fontSize: 20 }} />
<Typography level='body-xs' sx={{ mt: 0.5 }}> <Typography level='body-xs' sx={{ mt: 0.5 }}>
Edit Edit
</Typography> </Typography>
</Box> </Box>
</SwipeAction> </SwipeAction>
<SwipeAction onClick={() => handleDelete(historyEntry)}> <SwipeAction onClick={() => handleDelete(historyEntry)}>
<Box <Box
sx={{ sx={{
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
bgcolor: 'danger.softBg', bgcolor: 'danger.softBg',
color: 'danger.700', color: 'danger.700',
px: 3, px: 3,
height: '100%', height: '100%',
}} }}
> >
<DeleteIcon sx={{ fontSize: 20 }} /> <DeleteIcon sx={{ fontSize: 20 }} />
<Typography level='body-xs' sx={{ mt: 0.5 }}> <Typography level='body-xs' sx={{ mt: 0.5 }}>
Delete Delete
</Typography> </Typography>
</Box> </Box>
</SwipeAction> </SwipeAction>
</Box> </Box>
</TrailingActions> </TrailingActions>
} }
> >
<HistoryCard <HistoryCard
historyEntry={historyEntry} historyEntry={historyEntry}
performers={performers} performers={performers}
allHistory={choreHistory} allHistory={choreHistory}
index={index} index={index}
onViewDetails={() => { onViewDetails={() => {
setDetailModalConfig({ setDetailModalConfig({
isOpen: true, isOpen: true,
entry: historyEntry, entry: historyEntry,
performers, performers,
onClose: () => setDetailModalConfig({ isOpen: false }), onClose: () => setDetailModalConfig({ isOpen: false }),
onEdit: record => { onEdit: record => {
setDetailModalConfig({ isOpen: false }) setDetailModalConfig({ isOpen: false })
setEditHistory(record) setEditHistory(record)
setIsEditModalOpen(true) setIsEditModalOpen(true)
}, },
}) })
}} }}
pendingCommands={pendingByHistoryId[historyEntry.id] || []} pendingCommands={pendingByHistoryId[historyEntry.id] || []}
onViewNote={notes => { onViewNote={notes => {
setNoteViewerConfig({ setNoteViewerConfig({
isOpen: true, isOpen: true,
title: `Updated at ${fmt.dateTime(historyEntry.updatedAt)}`, title: `Updated at ${fmt.dateTime(historyEntry.updatedAt)}`,
content: notes, content: notes,
onClose: () => setNoteViewerConfig({ isOpen: false }), onClose: () => setNoteViewerConfig({ isOpen: false }),
}) })
}} }}
onToggleActions={() => { onToggleActions={() => {
const id = historyEntry.id || index const id = historyEntry.id || index
if (showMoreInfoId === id) { if (showMoreInfoId === id) {
setShowMoreInfoId(null) setShowMoreInfoId(null)
} else { } else {
setShowMoreInfoId(id) setShowMoreInfoId(id)
} }
}} }}
/> />
</SwipeableListItem> </SwipeableListItem>
))} ))}
</SwipeableList> </SwipeableList>
</Sheet> </Sheet>
)} )}
<EditHistoryModal <EditHistoryModal
config={{ config={{
@@ -604,7 +645,7 @@ const ChoreHistory = () => {
<ConfirmationModal config={confirmModalConfig} /> <ConfirmationModal config={confirmModalConfig} />
<NoteViewerModal config={noteViewerConfig} /> <NoteViewerModal config={noteViewerConfig} />
<HistoryDetailModal config={detailModalConfig} /> <HistoryDetailModal config={detailModalConfig} />
</Container> </Container>
) )
} }

View File

@@ -8,6 +8,7 @@ import { useResponsiveModal } from '../../hooks/useResponsiveModal'
import { useCreateChore } from '../../queries/ChoreQueries' import { useCreateChore } from '../../queries/ChoreQueries'
import { useCircleMembers, useUserProfile } from '../../queries/UserQueries' import { useCircleMembers, useUserProfile } from '../../queries/UserQueries'
import { isPlusAccount } from '../../utils/Helpers' import { isPlusAccount } from '../../utils/Helpers'
import { generateUUID } from '../../utils/UUID'
import { useLabels } from '../Labels/LabelQueries' import { useLabels } from '../Labels/LabelQueries'
import { useProjects } from '../Projects/ProjectQueries' import { useProjects } from '../Projects/ProjectQueries'
import { import {
@@ -106,7 +107,8 @@ const TaskInput = ({ onChoreUpdate, isModalOpen, onClose }) => {
const [showKeyboardShortcuts, setShowKeyboardShortcuts] = useState(false) const [showKeyboardShortcuts, setShowKeyboardShortcuts] = useState(false)
const [projectId, setProjectId] = useState(getInitialProject()) const [projectId, setProjectId] = useState(getInitialProject())
const [attachments, setAttachments] = useState([]) const [attachments, setAttachments] = useState([])
const [draftId, setDraftId] = useState(() => crypto.randomUUID())
const [draftId, setDraftId] = useState(() => generateUUID())
const [showScan, setShowScan] = useState(false) const [showScan, setShowScan] = useState(false)
const [scanAutoCapture, setScanAutoCapture] = useState(false) const [scanAutoCapture, setScanAutoCapture] = useState(false)
const [pendingPhotoUrl, setPendingPhotoUrl] = useState(null) const [pendingPhotoUrl, setPendingPhotoUrl] = useState(null)
@@ -595,7 +597,7 @@ const TaskInput = ({ onChoreUpdate, isModalOpen, onClose }) => {
setDueTime(null) setDueTime(null)
setUseCustomTime(false) setUseCustomTime(false)
setAttachments([]) setAttachments([])
setDraftId(crypto.randomUUID()) setDraftId(generateUUID())
} }
const createChore = () => { const createChore = () => {