diff --git a/extension/content.js b/extension/content.js index fddad97..858ad15 100644 --- a/extension/content.js +++ b/extension/content.js @@ -457,8 +457,11 @@ async function fetchBackendProfileNumber() { return null; } const data = await response.json(); - if (data && data.profile_number) { - return data.profile_number; + if (data && typeof data.profile_number !== 'undefined') { + const parsed = parseInt(data.profile_number, 10); + if (!Number.isNaN(parsed)) { + return parsed; + } } } catch (error) { console.warn('[FB Tracker] Failed to fetch profile state from backend:', error); @@ -499,25 +502,18 @@ function extractAuthorName(postElement) { return null; } -function storeProfileNumberLocally(profileNumber) { - chrome.storage.sync.set({ profileNumber }); -} - async function getProfileNumber() { - const backendProfile = await fetchBackendProfileNumber(); - if (backendProfile) { - storeProfileNumberLocally(backendProfile); - console.log('[FB Tracker] Profile number (backend):', backendProfile); - return backendProfile; + try { + const backendProfile = await fetchBackendProfileNumber(); + if (backendProfile) { + console.log('[FB Tracker] Profile number (backend):', backendProfile); + return backendProfile; + } + } catch (error) { + console.warn('[FB Tracker] Failed to resolve profile number from backend:', error); } - return new Promise((resolve) => { - chrome.storage.sync.get(['profileNumber'], (result) => { - const profile = result.profileNumber || 1; - console.log('[FB Tracker] Profile number (local):', profile); - resolve(profile); - }); - }); + return null; } // Extract post URL from post element @@ -2233,14 +2229,19 @@ function extractDeadlineFromPostText(postElement) { } const fullText = textNodes.join(' '); - const normalizedText = fullText.replace(/(\d)\s*([.:])\s*(\d)/g, '$1$2$3'); + const normalizedText = fullText + .normalize('NFKD') + .replace(/[\u0300-\u036f]/g, '') + .replace(/[^\S\r\n]+/g, ' ') + .replace(/(\d)\s*([.:])\s*(\d)/g, '$1$2$3'); + const normalizedLower = normalizedText.toLowerCase(); const today = new Date(); today.setHours(0, 0, 0, 0); const monthNames = { 'januar': 1, 'jan': 1, 'februar': 2, 'feb': 2, - 'märz': 3, 'mär': 3, 'maerz': 3, + 'märz': 3, 'mär': 3, 'maerz': 3, 'marz': 3, 'april': 4, 'apr': 4, 'mai': 5, 'juni': 6, 'jun': 6, @@ -2265,7 +2266,7 @@ function extractDeadlineFromPostText(postElement) { if (/^\s*(?:-|–|—|bis)\s*\d{1,2}\.\d{1,2}\./i.test(tail)) { return null; } - const timeMatch = /^\s*(?:\(|\[)?\s*(?:[,;:-]|\b)?\s*(?:um|ab|bis|gegen|spätestens)?\s*(?:den|dem|am)?\s*(?:ca\.?)?\s*(\d{1,2})(?:[:.](\d{2}))?\s*(?:uhr|h)?\s*(?:\)|\])?/i.exec(tail); + const timeMatch = /^\s*(?:\(|\[)?\s*(?:[,;:/|-]|\b)?\s*(?:um|ab|bis|gegen|spaetestens|spatestens|spätestens)?\s*(?:den|dem|am)?\s*(?:ca\.?)?\s*(\d{1,2})(?:[:.](\d{2}))?\s*(?:uhr|h)?\s*(?:\)|\])?/i.exec(tail); if (!timeMatch) { return null; } @@ -2291,24 +2292,98 @@ function extractDeadlineFromPostText(postElement) { return /\b(einschlie(?:ß|ss)lich|einschl\.?|inklusive|inkl\.)\b/.test(windowText); }; + const hasDeadlineKeywordNear = (text, index) => { + const windowStart = Math.max(0, index - 50); + const windowText = text.slice(windowStart, index).toLowerCase(); + return /\b(bis|spaetestens|spatestens|spätestens|teilnahmeschluss|einsendeschluss|anmeldeschluss|anmeldefrist|abgabeschluss|stichtag)\b/.test(windowText); + }; + + const addCandidateDate = (date, hasTime) => { + if (date > today) { + foundDates.push({ date, hasTime }); + } + }; + + const weekdayNames = { + montag: 1, + dienstag: 2, + mittwoch: 3, + donnerstag: 4, + freitag: 5, + samstag: 6, + sonntag: 0 + }; + + const getNextWeekdayDate = (weekday, preferNext) => { + const base = new Date(today); + let delta = (weekday - base.getDay() + 7) % 7; + if (delta === 0 && preferNext) { + delta = 7; + } + base.setDate(base.getDate() + delta); + return base; + }; + const foundDates = []; - const rangePattern = /\b(\d{1,2})\.(\d{1,2})\.(\d{2,4})\s*(?:-|–|—|bis)\s*(\d{1,2})\.(\d{1,2})\.(\d{2,4})\b/i; - const rangeMatch = rangePattern.exec(normalizedText); - if (rangeMatch) { - const endDay = parseInt(rangeMatch[4], 10); - const endMonth = parseInt(rangeMatch[5], 10); - let endYear = parseInt(rangeMatch[6], 10); + const rangePatterns = [ + // DD.MM.YYYY - DD.MM.YYYY + /\b(\d{1,2})\.(\d{1,2})\.(\d{2,4})\s*(?:-|–|—|bis)\s*(\d{1,2})\.(\d{1,2})\.(\d{2,4})\b/i, + // DD.–DD.MM.YYYY (start day only) + /\b(\d{1,2})\.\s*(?:-|–|—|bis)\s*(\d{1,2})\.(\d{1,2})\.(\d{2,4})\b/i, + // vom DD.MM bis DD.MM.YYYY + /\bvom\s*(\d{1,2})\.(\d{1,2})\.?\s*(?:-|–|—|bis)\s*(\d{1,2})\.(\d{1,2})\.(\d{2,4})\b/i + ]; + + for (const rangePattern of rangePatterns) { + const rangeMatch = rangePattern.exec(normalizedText); + if (!rangeMatch) { + continue; + } + + const endDay = parseInt(rangeMatch[rangeMatch.length - 3], 10); + const endMonth = parseInt(rangeMatch[rangeMatch.length - 2], 10); + let endYear = parseInt(rangeMatch[rangeMatch.length - 1], 10); + if (endYear < 100) { endYear += 2000; } + if (endMonth >= 1 && endMonth <= 12 && endDay >= 1 && endDay <= 31) { - const endDate = new Date(endYear, endMonth - 1, endDay, 0, 0, 0, 0); + const endDate = new Date(endYear, endMonth - 1, endDay, 23, 59, 0, 0); if (endDate.getMonth() === endMonth - 1 && endDate.getDate() === endDay && endDate > today) { return toDateTimeLocalString(endDate); } } } + // Weekday-only patterns like "bis nächsten Mittwoch" or "Sonntag, 23:59 Uhr" + const weekdayPattern = /\b(montag|dienstag|mittwoch|donnerstag|freitag|samstag|sonntag)\b/gi; + let weekdayMatch; + while ((weekdayMatch = weekdayPattern.exec(normalizedLower)) !== null) { + const weekdayKey = weekdayMatch[1].toLowerCase(); + const weekday = weekdayNames[weekdayKey]; + if (typeof weekday !== 'number') { + continue; + } + + const matchIndex = weekdayMatch.index; + const windowStart = Math.max(0, matchIndex - 30); + const windowText = normalizedLower.slice(windowStart, matchIndex); + const preferNext = /\b(naechst|nachst|kommend)\b/.test(windowText); + const date = getNextWeekdayDate(weekday, preferNext); + const timeInfo = extractTimeAfterIndex(normalizedText, weekdayPattern.lastIndex); + const hasTime = Boolean(timeInfo); + + if (timeInfo) { + date.setHours(timeInfo.hour, timeInfo.minute, 0, 0); + } else if (hasInclusiveKeywordNear(normalizedText, matchIndex) || hasDeadlineKeywordNear(normalizedText, matchIndex)) { + date.setHours(23, 59, 0, 0); + } + + const recordHasTime = hasTime || hasInclusiveKeywordNear(normalizedText, matchIndex) || hasDeadlineKeywordNear(normalizedText, matchIndex); + addCandidateDate(date, recordHasTime); + } + for (const pattern of patterns) { let match; while ((match = pattern.exec(normalizedText)) !== null) { @@ -2332,27 +2407,25 @@ function extractDeadlineFromPostText(postElement) { const hasTime = Boolean(timeInfo); if (timeInfo) { date.setHours(timeInfo.hour, timeInfo.minute, 0, 0); - } else if (hasInclusiveKeywordNear(normalizedText, matchIndex)) { + } else if (hasInclusiveKeywordNear(normalizedText, matchIndex) || hasDeadlineKeywordNear(normalizedText, matchIndex)) { date.setHours(23, 59, 0, 0); } const hasInclusiveTime = hasInclusiveKeywordNear(normalizedText, matchIndex); - const recordHasTime = hasTime || hasInclusiveTime; + const hasDeadlineTime = hasDeadlineKeywordNear(normalizedText, matchIndex); + const recordHasTime = hasTime || hasInclusiveTime || hasDeadlineTime; if (hasInclusiveTime && !hasTime) { date.setHours(23, 59, 0, 0); } - // Only add if date is in the future - if (date > today) { - foundDates.push({ date, hasTime: recordHasTime }); - } + addCandidateDate(date, recordHasTime); } } } } // Pattern for "12. Oktober" or "12 Oktober" - const monthPattern = /\b(\d{1,2})\.?\s*(januar|jan|februar|feb|märz|mär|maerz|april|apr|mai|juni|jun|juli|jul|august|aug|september|sep|sept|oktober|okt|november|nov|dezember|dez)\s*(\d{2,4})?\b/gi; + const monthPattern = /\b(\d{1,2})\.?\s*(januar|jan|februar|feb|märz|mär|maerz|marz|april|apr|mai|juni|jun|juli|jul|august|aug|september|sep|sept|oktober|okt|november|nov|dezember|dez)\s*(\d{2,4})?\b/gi; let monthMatch; while ((monthMatch = monthPattern.exec(normalizedText)) !== null) { const day = parseInt(monthMatch[1], 10); @@ -2376,12 +2449,13 @@ function extractDeadlineFromPostText(postElement) { const hasTime = Boolean(timeInfo); if (timeInfo) { date.setHours(timeInfo.hour, timeInfo.minute, 0, 0); - } else if (hasInclusiveKeywordNear(normalizedText, matchIndex)) { + } else if (hasInclusiveKeywordNear(normalizedText, matchIndex) || hasDeadlineKeywordNear(normalizedText, matchIndex)) { date.setHours(23, 59, 0, 0); } const hasInclusiveTime = hasInclusiveKeywordNear(normalizedText, matchIndex); - const recordHasTime = hasTime || hasInclusiveTime; + const hasDeadlineTime = hasDeadlineKeywordNear(normalizedText, matchIndex); + const recordHasTime = hasTime || hasInclusiveTime || hasDeadlineTime; if (hasInclusiveTime && !hasTime) { date.setHours(23, 59, 0, 0); } @@ -2390,23 +2464,17 @@ function extractDeadlineFromPostText(postElement) { if (date <= today) { date.setFullYear(year + 1); } - foundDates.push({ date, hasTime: recordHasTime }); + addCandidateDate(date, recordHasTime); } } } - // Return the earliest future date + // Prefer dates with explicit time; otherwise fall back to earliest date. if (foundDates.length > 0) { - foundDates.sort((a, b) => { - const diff = a.date - b.date; - if (diff !== 0) { - return diff; - } - if (a.hasTime && !b.hasTime) return -1; - if (!a.hasTime && b.hasTime) return 1; - return 0; - }); - return toDateTimeLocalString(foundDates[0].date); + const withTime = foundDates.filter((entry) => entry.hasTime); + const candidates = withTime.length ? withTime : foundDates; + candidates.sort((a, b) => a.date - b.date); + return toDateTimeLocalString(candidates[0].date); } return null; @@ -2735,6 +2803,9 @@ async function renderTrackedStatus({ return { hidden: false }; } + const parsedProfile = parseInt(profileNumber, 10); + const effectiveProfileNumber = Number.isNaN(parsedProfile) ? null : parsedProfile; + if (postData.id) { container.dataset.postId = postData.id; } @@ -2760,9 +2831,12 @@ async function renderTrackedStatus({ }) .filter(Boolean) : Array.from({ length: Math.max(1, Math.min(5, parseInt(postData.target_count, 10) || 1)) }, (_, index) => index + 1); - const isCurrentProfileRequired = requiredProfiles.includes(profileNumber); - const canCurrentProfileCheck = isCurrentProfileRequired && postData.next_required_profile === profileNumber; - const isCurrentProfileDone = checks.some(check => check.profile_number === profileNumber); + const isCurrentProfileRequired = effectiveProfileNumber !== null && requiredProfiles.includes(effectiveProfileNumber); + const parsedNextRequired = parseInt(postData.next_required_profile, 10); + const nextRequiredProfile = Number.isNaN(parsedNextRequired) ? null : parsedNextRequired; + const canCurrentProfileCheck = isCurrentProfileRequired && nextRequiredProfile === effectiveProfileNumber; + const isCurrentProfileDone = effectiveProfileNumber !== null + && checks.some(check => check.profile_number === effectiveProfileNumber); if (isFeedHome && isCurrentProfileDone) { if (isDialogContext) { @@ -2815,6 +2889,23 @@ async function renderTrackedStatus({ ✓ Bestätigen `; + } else if (!isExpired && !completed && !isCurrentProfileDone && effectiveProfileNumber === null) { + statusHtml += ` + + `; } else if (isCurrentProfileDone) { statusHtml += `