Aktueller Stand

This commit is contained in:
2026-01-13 13:46:36 +01:00
parent 9675e73406
commit b3ca39ddc2
4 changed files with 276 additions and 78 deletions

View File

@@ -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
</button>
`;
} else if (!isExpired && !completed && !isCurrentProfileDone && effectiveProfileNumber === null) {
statusHtml += `
<button class="fb-tracker-check-btn" title="Profilstatus nicht geladen" style="
padding: 4px 12px;
background-color: #9ca3af;
color: white;
border: none;
border-radius: 4px;
font-weight: 600;
cursor: not-allowed;
font-size: 13px;
white-space: nowrap;
margin-left: 8px;
" disabled>
✓ Bestätigen
</button>
`;
} else if (isCurrentProfileDone) {
statusHtml += `
<div style="color: #42b72a; font-size: 12px; white-space: nowrap; margin-left: 8px;">
@@ -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();