From d207f7e2b20ae28d9dca1ad43f25c8c442cd5189 Mon Sep 17 00:00:00 2001 From: Mo Tarbin Date: Thu, 9 Jul 2026 14:44:05 -0400 Subject: [PATCH] fix: implement UUID generation utility and replace crypto.randomUUID usage (to support genertaion when we are in http )in task and chore components --- src/utils/UUID.js | 10 + src/views/Authorization/LoginSettings.jsx | 66 ++++- src/views/ChoreEdit/ChoreEdit.jsx | 3 +- src/views/History/ChoreHistory.jsx | 301 ++++++++++++---------- src/views/components/AddTaskModal.jsx | 6 +- 5 files changed, 251 insertions(+), 135 deletions(-) create mode 100644 src/utils/UUID.js diff --git a/src/utils/UUID.js b/src/utils/UUID.js new file mode 100644 index 0000000..49b1d2c --- /dev/null +++ b/src/utils/UUID.js @@ -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) + }) +} diff --git a/src/views/Authorization/LoginSettings.jsx b/src/views/Authorization/LoginSettings.jsx index e8fe85e..914e0e6 100644 --- a/src/views/Authorization/LoginSettings.jsx +++ b/src/views/Authorization/LoginSettings.jsx @@ -39,13 +39,14 @@ const LoginSettings = () => { } const testConnection = async url => { + const testURL = url.replace(/\/+$/, '') + '/api/v1/resource' const controller = new AbortController() const timeoutId = setTimeout( () => controller.abort(), CONNECTION_TIMEOUT_MS, ) + try { - const testURL = url.replace(/\/+$/, '') + '/api/v1/resource' const response = await fetch(testURL, { method: 'GET', signal: controller.signal, @@ -55,18 +56,79 @@ const LoginSettings = () => { if (response.status < 500) { 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 { ok: false, message: `Server responded with error ${response.status}. Please check your Donetick server.`, } } catch (err) { clearTimeout(timeoutId) + if (err.name === 'AbortError') { return { 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 { ok: false, message: diff --git a/src/views/ChoreEdit/ChoreEdit.jsx b/src/views/ChoreEdit/ChoreEdit.jsx index 7ac6cd4..23f3fee 100644 --- a/src/views/ChoreEdit/ChoreEdit.jsx +++ b/src/views/ChoreEdit/ChoreEdit.jsx @@ -58,6 +58,7 @@ import { UploadChoreAttachment, } from '../../utils/Fetcher' import { isPlusAccount, resolvePhotoURL } from '../../utils/Helpers' +import { generateUUID } from '../../utils/UUID' import Priorities from '../../utils/Priorities.jsx' import { getIconComponent } from '../../utils/ProjectIcons' import { getSafeBottomPadding } from '../../utils/SafeAreaUtils.js' @@ -131,7 +132,7 @@ const ChoreEdit = () => { const [createdBy, setCreatedBy] = useState(0) const [errors, setErrors] = useState({}) const [attemptToSave, setAttemptToSave] = useState(false) - const [draftId] = useState(() => crypto.randomUUID()) + const [draftId] = useState(() => generateUUID()) const [attachments, setAttachments] = useState([]) const [isUploadingAttachment, setIsUploadingAttachment] = useState(false) const [addLabelModalOpen, setAddLabelModalOpen] = useState(false) diff --git a/src/views/History/ChoreHistory.jsx b/src/views/History/ChoreHistory.jsx index 84ac352..3298408 100644 --- a/src/views/History/ChoreHistory.jsx +++ b/src/views/History/ChoreHistory.jsx @@ -99,12 +99,42 @@ const ChoreHistory = () => { type: 'multi-select', icon: , options: [ - { value: ChoreHistoryStatus.COMPLETED, label: 'Completed', color: 'success', icon: }, - { value: ChoreHistoryStatus.SKIPPED, label: 'Skipped', color: 'warning', icon: }, - { value: ChoreHistoryStatus.PENDING_APPROVAL, label: 'Pending', color: 'neutral', icon: }, - { value: ChoreHistoryStatus.REJECTED, label: 'Rejected', color: 'danger', icon: }, - { value: 5, label: 'Missed', color: 'danger', icon: }, - { value: 6, label: 'Rescheduled', color: 'warning', icon: }, + { + value: ChoreHistoryStatus.COMPLETED, + label: 'Completed', + color: 'success', + icon: , + }, + { + value: ChoreHistoryStatus.SKIPPED, + label: 'Skipped', + color: 'warning', + icon: , + }, + { + value: ChoreHistoryStatus.PENDING_APPROVAL, + label: 'Pending', + color: 'neutral', + icon: , + }, + { + value: ChoreHistoryStatus.REJECTED, + label: 'Rejected', + color: 'danger', + icon: , + }, + { + value: 5, + label: 'Missed', + color: 'danger', + icon: , + }, + { + value: 6, + label: 'Rescheduled', + color: 'warning', + icon: , + }, ], filterFn: (item, values) => values.includes(item.status), }, @@ -143,8 +173,23 @@ const ChoreHistory = () => { [performers], ) - const { filteredData: filteredHistory, activeFilters, setFilter, clearAll, activeFilterCount } = - useFilter(choreHistory, filterDefs) + const { + 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 => { showConfirmation( @@ -296,7 +341,7 @@ const ChoreHistory = () => { {/* Enhanced Header Section */} - {/* */} + {/* */} {/* Statistics Cards Grid - Compact Design */} @@ -374,9 +419,8 @@ const ChoreHistory = () => { {/* History Section Header */} - + - { - - - - {filteredHistory.length === 0 && activeFilterCount > 0 && ( + + + + {sortedHistory.length === 0 && activeFilterCount > 0 && ( { )} - {filteredHistory.length > 0 && ( - - {/* Chore History List (Updated Style) */} + {sortedHistory.length > 0 && ( + + {/* Chore History List (Updated Style) */} - - {filteredHistory.map((historyEntry, index) => ( - - - handleEdit(historyEntry)}> - - - - Edit - - - - handleDelete(historyEntry)}> - - - - Delete - - - - - - } - > - { - setDetailModalConfig({ - isOpen: true, - entry: historyEntry, - performers, - onClose: () => setDetailModalConfig({ isOpen: false }), - onEdit: record => { - setDetailModalConfig({ isOpen: false }) - setEditHistory(record) - setIsEditModalOpen(true) - }, - }) - }} - pendingCommands={pendingByHistoryId[historyEntry.id] || []} - onViewNote={notes => { - setNoteViewerConfig({ - isOpen: true, - title: `Updated at ${fmt.dateTime(historyEntry.updatedAt)}`, - content: notes, - onClose: () => setNoteViewerConfig({ isOpen: false }), - }) - }} - onToggleActions={() => { - const id = historyEntry.id || index - if (showMoreInfoId === id) { - setShowMoreInfoId(null) - } else { - setShowMoreInfoId(id) - } - }} - /> - - ))} - - + + {sortedHistory.map((historyEntry, index) => ( + + + handleEdit(historyEntry)}> + + + + Edit + + + + handleDelete(historyEntry)}> + + + + Delete + + + + + + } + > + { + setDetailModalConfig({ + isOpen: true, + entry: historyEntry, + performers, + onClose: () => setDetailModalConfig({ isOpen: false }), + onEdit: record => { + setDetailModalConfig({ isOpen: false }) + setEditHistory(record) + setIsEditModalOpen(true) + }, + }) + }} + pendingCommands={pendingByHistoryId[historyEntry.id] || []} + onViewNote={notes => { + setNoteViewerConfig({ + isOpen: true, + title: `Updated at ${fmt.dateTime(historyEntry.updatedAt)}`, + content: notes, + onClose: () => setNoteViewerConfig({ isOpen: false }), + }) + }} + onToggleActions={() => { + const id = historyEntry.id || index + if (showMoreInfoId === id) { + setShowMoreInfoId(null) + } else { + setShowMoreInfoId(id) + } + }} + /> + + ))} + + )} { - + ) } diff --git a/src/views/components/AddTaskModal.jsx b/src/views/components/AddTaskModal.jsx index e73c121..fcf6207 100644 --- a/src/views/components/AddTaskModal.jsx +++ b/src/views/components/AddTaskModal.jsx @@ -8,6 +8,7 @@ import { useResponsiveModal } from '../../hooks/useResponsiveModal' import { useCreateChore } from '../../queries/ChoreQueries' import { useCircleMembers, useUserProfile } from '../../queries/UserQueries' import { isPlusAccount } from '../../utils/Helpers' +import { generateUUID } from '../../utils/UUID' import { useLabels } from '../Labels/LabelQueries' import { useProjects } from '../Projects/ProjectQueries' import { @@ -106,7 +107,8 @@ const TaskInput = ({ onChoreUpdate, isModalOpen, onClose }) => { const [showKeyboardShortcuts, setShowKeyboardShortcuts] = useState(false) const [projectId, setProjectId] = useState(getInitialProject()) const [attachments, setAttachments] = useState([]) - const [draftId, setDraftId] = useState(() => crypto.randomUUID()) + + const [draftId, setDraftId] = useState(() => generateUUID()) const [showScan, setShowScan] = useState(false) const [scanAutoCapture, setScanAutoCapture] = useState(false) const [pendingPhotoUrl, setPendingPhotoUrl] = useState(null) @@ -595,7 +597,7 @@ const TaskInput = ({ onChoreUpdate, isModalOpen, onClose }) => { setDueTime(null) setUseCustomTime(false) setAttachments([]) - setDraftId(crypto.randomUUID()) + setDraftId(generateUUID()) } const createChore = () => {