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 += `
@@ -2885,17 +2976,21 @@ async function renderTrackedStatus({ const checkBtn = container.querySelector('.fb-tracker-check-btn'); if (checkBtn) { checkBtn.addEventListener('click', async () => { + if (effectiveProfileNumber === null) { + showToast('Profilstatus nicht geladen. Bitte Tracker neu laden.', 'error'); + return; + } checkBtn.disabled = true; checkBtn.textContent = 'Wird bestätigt...'; - const result = await markPostChecked(postData.id, profileNumber, { returnError: true }); + const result = await markPostChecked(postData.id, effectiveProfileNumber, { returnError: true }); if (result && !result.error) { await renderTrackedStatus({ container, postElement, postData: result, - profileNumber, + profileNumber: effectiveProfileNumber, isFeedHome, isDialogContext, manualHideInfo, @@ -3331,6 +3426,13 @@ async function createTrackerUI(postElement, buttonBar, postNum = '?', options = const targetCount = parseInt(selectElement.value, 10); console.log('[FB Tracker] Add button clicked, target:', targetCount); + if (!profileNumber) { + showToast('Profilstatus nicht geladen. Bitte Tracker neu laden.', 'error'); + addButton.disabled = false; + addButton.textContent = 'Erneut versuchen'; + return; + } + addButton.disabled = true; addButton.textContent = 'Wird hinzugefügt...'; @@ -5277,6 +5379,11 @@ async function handleSelectionAIRequest(selectionText, sendResponse) { showToast('AI verarbeitet Auswahl...', 'info'); const profileNumber = await getProfileNumber(); + if (!profileNumber) { + showToast('Profilstatus nicht geladen. Bitte Tracker neu laden.', 'error'); + sendResponse({ error: 'profile-missing' }); + return; + } const comment = await generateAIComment(normalizedSelection, profileNumber, {}); if (!comment) { @@ -5597,6 +5704,10 @@ async function addAICommentButton(container, postElement) { } const effectiveProfile = profileNumber || await getProfileNumber(); + if (!effectiveProfile) { + showToast('Profilstatus nicht geladen. Bitte Tracker neu laden.', 'error'); + return; + } const decodedUrl = getDecodedPostUrl(); const isFeedHomeFlag = container.dataset.isFeedHome === '1'; const isDialogFlag = container.dataset.isDialogContext === '1'; @@ -6079,22 +6190,26 @@ async function addAICommentButton(container, postElement) { throw new Error('Konnte Post-Text nicht extrahieren'); } - selectionKeys.forEach((key) => { - if (key) { - postSelectionCache.delete(key); - } - }); + selectionKeys.forEach((key) => { + if (key) { + postSelectionCache.delete(key); + } + }); - const additionalNote = postContext ? (postAdditionalNotes.get(postContext) || '') : ''; - if (additionalNote) { - postText = `${postText}\n\nZusatzinfo:\n${additionalNote}`; - } + const additionalNote = postContext ? (postAdditionalNotes.get(postContext) || '') : ''; + if (additionalNote) { + postText = `${postText}\n\nZusatzinfo:\n${additionalNote}`; + } - throwIfCancelled(); + throwIfCancelled(); - console.log('[FB Tracker] Generating AI comment for:', postText.substring(0, 100)); + console.log('[FB Tracker] Generating AI comment for:', postText.substring(0, 100)); const profileNumber = await getProfileNumber(); + if (!profileNumber) { + showToast('Profilstatus nicht geladen. Bitte Tracker neu laden.', 'error'); + return; + } throwIfCancelled(); diff --git a/web/app.js b/web/app.js index bedb35d..2e91a14 100644 --- a/web/app.js +++ b/web/app.js @@ -1287,13 +1287,97 @@ function buildBookmarkSearchUrl(query) { return searchUrl.toString(); } +function splitBookmarkGroupOptions(rawGroup) { + if (typeof rawGroup !== 'string') { + return []; + } + + return rawGroup + .split(',') + .map((entry) => entry.trim()) + .filter(Boolean); +} + +function expandBookmarkQueryVariants(baseQuery) { + const trimmed = typeof baseQuery === 'string' ? baseQuery.trim() : ''; + if (!trimmed) { + return []; + } + + const variants = ['']; + let cursor = 0; + + while (cursor < trimmed.length) { + const openIndex = trimmed.indexOf('(', cursor); + if (openIndex === -1) { + const tail = trimmed.slice(cursor); + for (let i = 0; i < variants.length; i += 1) { + variants[i] = `${variants[i]}${tail}`; + } + break; + } + + const closeIndex = trimmed.indexOf(')', openIndex + 1); + if (closeIndex === -1) { + const tail = trimmed.slice(cursor); + for (let i = 0; i < variants.length; i += 1) { + variants[i] = `${variants[i]}${tail}`; + } + break; + } + + const before = trimmed.slice(cursor, openIndex); + if (before) { + for (let i = 0; i < variants.length; i += 1) { + variants[i] = `${variants[i]}${before}`; + } + } + + const groupRaw = trimmed.slice(openIndex + 1, closeIndex); + const options = splitBookmarkGroupOptions(groupRaw); + + if (options.length) { + const expanded = []; + variants.forEach((prefix) => { + options.forEach((option) => { + expanded.push(`${prefix}${option}`); + }); + }); + variants.splice(0, variants.length, ...expanded); + } else { + const literalGroup = trimmed.slice(openIndex, closeIndex + 1); + for (let i = 0; i < variants.length; i += 1) { + variants[i] = `${variants[i]}${literalGroup}`; + } + } + + cursor = closeIndex + 1; + } + + return variants + .map((variant) => variant.trim()) + .filter(Boolean); +} + function buildBookmarkSearchQueries(baseQuery) { const trimmed = typeof baseQuery === 'string' ? baseQuery.trim() : ''; if (!trimmed) { return [...BOOKMARK_SUFFIXES]; } - return BOOKMARK_SUFFIXES.map((suffix) => `${trimmed} ${suffix}`.trim()); + const baseVariants = expandBookmarkQueryVariants(trimmed); + if (!baseVariants.length) { + return BOOKMARK_SUFFIXES.map((suffix) => `${trimmed} ${suffix}`.trim()); + } + + const queries = []; + baseVariants.forEach((variant) => { + BOOKMARK_SUFFIXES.forEach((suffix) => { + queries.push(`${variant} ${suffix}`.trim()); + }); + }); + + return [...new Set(queries)]; } function openBookmarkQueries(baseQuery) { diff --git a/web/automation.css b/web/automation.css index ed2704f..bb0afbd 100644 --- a/web/automation.css +++ b/web/automation.css @@ -283,24 +283,23 @@ .automation-view .auto-table .sort-indicator { display: inline-block; margin-left: 6px; - width: 10px; - height: 10px; - border: 5px solid transparent; - border-bottom: 0; - border-left: 0; - transform: rotate(45deg); - opacity: 0.25; + width: 18px; + height: 18px; + background-color: var(--automation-muted); + -webkit-mask: url("data:image/svg+xml;utf8,") no-repeat center / contain; + mask: url("data:image/svg+xml;utf8,") no-repeat center / contain; + opacity: 0.35; } .automation-view .auto-table th.sort-asc .sort-indicator { - border-top: 6px solid var(--automation-accent-2); - transform: rotate(225deg); + background-color: var(--automation-accent-2); + transform: rotate(180deg); opacity: 0.9; } .automation-view .auto-table th.sort-desc .sort-indicator { - border-top: 6px solid var(--automation-accent-2); - transform: rotate(45deg); + background-color: var(--automation-accent-2); + transform: rotate(0deg); opacity: 0.9; } diff --git a/web/automation.js b/web/automation.js index e25a0ff..ebb95dd 100644 --- a/web/automation.js +++ b/web/automation.js @@ -637,7 +637,7 @@
${formatDateTime(req.next_run_at)} - ${req.run_until ? formatRelative(req.run_until) : '—'} + ${req.run_until ? formatRelative(req.run_until) : '—'}
${formatDateTime(req.run_until)}