Skip to content

Commit a82441f

Browse files
Clément VALENTINclaude
andcommitted
fix(web): add defensive type checks for offpeak_hours parsing
Add runtime type checks to prevent "W.match is not a function" error when offpeak_hours contains non-string values. Changes include: - Add typeof checks before calling .match() on values - Wrap parsing logic in try/catch to handle unexpected data formats - Centralize default range value to reduce duplication - Log errors for debugging when parsing fails 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent e6c34e7 commit a82441f

File tree

1 file changed

+60
-37
lines changed

1 file changed

+60
-37
lines changed

apps/web/src/components/PDLCard.tsx

Lines changed: 60 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,12 @@ export default function PDLCard({ pdl, onViewDetails, onDelete, isDemo = false,
2929
const saveTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
3030
const mobileMenuRef = useRef<HTMLDivElement | null>(null)
3131
const [offpeakRanges, setOffpeakRanges] = useState<Array<{startHour: string, startMin: string, endHour: string, endMin: string}>>(() => {
32-
const parseRange = (range: string) => {
32+
const defaultRange = { startHour: '00', startMin: '00', endHour: '00', endMin: '00' }
33+
34+
const parseRange = (range: unknown) => {
35+
// Safety check: ensure range is a string
36+
if (typeof range !== 'string') return defaultRange
37+
3338
// Try format "HH:MM-HH:MM" (array format)
3439
let match = range.match(/^(\d{1,2}):(\d{2})-(\d{1,2}):(\d{2})$/)
3540
if (match) {
@@ -60,10 +65,13 @@ export default function PDLCard({ pdl, onViewDetails, onDelete, isDemo = false,
6065
}
6166
}
6267

63-
return { startHour: '00', startMin: '00', endHour: '00', endMin: '00' }
68+
return defaultRange
6469
}
6570

66-
const parseAllRanges = (str: string) => {
71+
const parseAllRanges = (str: unknown) => {
72+
// Safety check: ensure str is a string
73+
if (typeof str !== 'string') return [defaultRange]
74+
6775
const results = []
6876
// Check if it's Enedis format with parentheses
6977
const enedisMatch = str.match(/HC\s*\(([^)]+)\)/i)
@@ -87,30 +95,40 @@ export default function PDLCard({ pdl, onViewDetails, onDelete, isDemo = false,
8795
return [parseRange(str)]
8896
}
8997

90-
if (!pdl.offpeak_hours) return [{ startHour: '00', startMin: '00', endHour: '00', endMin: '00' }]
98+
if (!pdl.offpeak_hours) return [defaultRange]
9199

92-
if (Array.isArray(pdl.offpeak_hours)) {
93-
// Filter to only strings and parse
94-
const stringValues = pdl.offpeak_hours.filter((v): v is string => typeof v === 'string')
95-
return stringValues.length > 0
96-
? stringValues.flatMap(parseAllRanges)
97-
: [{ startHour: '00', startMin: '00', endHour: '00', endMin: '00' }]
98-
}
100+
try {
101+
if (Array.isArray(pdl.offpeak_hours)) {
102+
// Filter to only strings and parse
103+
const stringValues = pdl.offpeak_hours.filter((v): v is string => typeof v === 'string')
104+
return stringValues.length > 0
105+
? stringValues.flatMap(parseAllRanges)
106+
: [defaultRange]
107+
}
99108

100-
// Legacy format: convert object to array and deduplicate
101-
// Handle nested arrays (e.g., {"ranges": ["22:00-06:00"]})
102-
const rawValues = Object.values(pdl.offpeak_hours).filter(Boolean)
103-
const values = rawValues.flatMap(v => Array.isArray(v) ? v : [v]).filter((v): v is string => typeof v === 'string')
104-
const uniqueValues = Array.from(new Set(values))
105-
return uniqueValues.length > 0
106-
? uniqueValues.flatMap(parseAllRanges)
107-
: [{ startHour: '00', startMin: '00', endHour: '00', endMin: '00' }]
109+
// Legacy format: convert object to array and deduplicate
110+
// Handle nested arrays (e.g., {"ranges": ["22:00-06:00"]})
111+
const rawValues = Object.values(pdl.offpeak_hours).filter(Boolean)
112+
const values = rawValues.flatMap(v => Array.isArray(v) ? v : [v]).filter((v): v is string => typeof v === 'string')
113+
const uniqueValues = Array.from(new Set(values))
114+
return uniqueValues.length > 0
115+
? uniqueValues.flatMap(parseAllRanges)
116+
: [defaultRange]
117+
} catch (error) {
118+
console.error('[PDLCard] Error parsing offpeak_hours:', error, pdl.offpeak_hours)
119+
return [defaultRange]
120+
}
108121
})
109122
const queryClient = useQueryClient()
110123

111124
// Sync edited values with PDL changes
112125
useEffect(() => {
113-
const parseAllRanges = (str: string) => {
126+
const defaultRange = { startHour: '00', startMin: '00', endHour: '00', endMin: '00' }
127+
128+
const parseAllRanges = (str: unknown) => {
129+
// Safety check: ensure str is a string
130+
if (typeof str !== 'string') return [defaultRange]
131+
114132
const results = []
115133
// Check if it's Enedis format with parentheses
116134
const enedisMatch = str.match(/HC\s*\(([^)]+)\)/i)
@@ -128,7 +146,7 @@ export default function PDLCard({ pdl, onViewDetails, onDelete, isDemo = false,
128146
})
129147
}
130148
}
131-
return results.length > 0 ? results : [{ startHour: '00', startMin: '00', endHour: '00', endMin: '00' }]
149+
return results.length > 0 ? results : [defaultRange]
132150
}
133151

134152
// Try format "HH:MM-HH:MM" (array format)
@@ -142,27 +160,32 @@ export default function PDLCard({ pdl, onViewDetails, onDelete, isDemo = false,
142160
}]
143161
}
144162

145-
return [{ startHour: '00', startMin: '00', endHour: '00', endMin: '00' }]
163+
return [defaultRange]
146164
}
147165

148166
setEditedName(pdl.name || '')
149167
setEditedPower(pdl.subscribed_power?.toString() || '')
150168

151-
if (!pdl.offpeak_hours) {
152-
setOffpeakRanges([{ startHour: '00', startMin: '00', endHour: '00', endMin: '00' }])
153-
} else if (Array.isArray(pdl.offpeak_hours)) {
154-
// Filter to only strings and parse
155-
const stringValues = pdl.offpeak_hours.filter((v): v is string => typeof v === 'string')
156-
const parsed = stringValues.flatMap(parseAllRanges)
157-
setOffpeakRanges(parsed.length > 0 ? parsed : [{ startHour: '00', startMin: '00', endHour: '00', endMin: '00' }])
158-
} else {
159-
// Legacy format: convert object to array and deduplicate
160-
// Handle nested arrays (e.g., {"ranges": ["22:00-06:00"]})
161-
const rawValues = Object.values(pdl.offpeak_hours).filter(Boolean)
162-
const values = rawValues.flatMap(v => Array.isArray(v) ? v : [v]).filter((v): v is string => typeof v === 'string')
163-
const uniqueValues = Array.from(new Set(values))
164-
const parsed = uniqueValues.flatMap(parseAllRanges)
165-
setOffpeakRanges(parsed.length > 0 ? parsed : [{ startHour: '00', startMin: '00', endHour: '00', endMin: '00' }])
169+
try {
170+
if (!pdl.offpeak_hours) {
171+
setOffpeakRanges([defaultRange])
172+
} else if (Array.isArray(pdl.offpeak_hours)) {
173+
// Filter to only strings and parse
174+
const stringValues = pdl.offpeak_hours.filter((v): v is string => typeof v === 'string')
175+
const parsed = stringValues.flatMap(parseAllRanges)
176+
setOffpeakRanges(parsed.length > 0 ? parsed : [defaultRange])
177+
} else {
178+
// Legacy format: convert object to array and deduplicate
179+
// Handle nested arrays (e.g., {"ranges": ["22:00-06:00"]})
180+
const rawValues = Object.values(pdl.offpeak_hours).filter(Boolean)
181+
const values = rawValues.flatMap(v => Array.isArray(v) ? v : [v]).filter((v): v is string => typeof v === 'string')
182+
const uniqueValues = Array.from(new Set(values))
183+
const parsed = uniqueValues.flatMap(parseAllRanges)
184+
setOffpeakRanges(parsed.length > 0 ? parsed : [defaultRange])
185+
}
186+
} catch (error) {
187+
console.error('[PDLCard] Error parsing offpeak_hours in effect:', error, pdl.offpeak_hours)
188+
setOffpeakRanges([defaultRange])
166189
}
167190
}, [pdl.id, pdl.name, pdl.subscribed_power, pdl.offpeak_hours])
168191

0 commit comments

Comments
 (0)