Aktueller Stand

This commit is contained in:
MDeeApp
2025-10-19 12:14:03 +02:00
parent 327a663bcf
commit 9745d38995
6 changed files with 1304 additions and 189 deletions

View File

@@ -486,6 +486,49 @@ function getPostUrl(postElement, postNum = '?') {
return { url: '', allCandidates: [] };
}
function expandPhotoUrlHostVariants(url) {
if (typeof url !== 'string' || !url) {
return [];
}
try {
const parsed = new URL(url);
const hostname = parsed.hostname.toLowerCase();
if (!hostname.endsWith('facebook.com')) {
return [];
}
const pathname = parsed.pathname.toLowerCase();
if (!pathname.startsWith('/photo')) {
return [];
}
const search = parsed.search || '';
const protocol = parsed.protocol || 'https:';
const hosts = ['www.facebook.com', 'facebook.com', 'm.facebook.com'];
const variants = [];
for (const candidateHost of hosts) {
if (candidateHost === hostname) {
continue;
}
const candidateUrl = `${protocol}//${candidateHost}${parsed.pathname}${search}`;
const normalizedVariant = normalizeFacebookPostUrl(candidateUrl);
if (
normalizedVariant
&& normalizedVariant !== url
&& !variants.includes(normalizedVariant)
) {
variants.push(normalizedVariant);
}
}
return variants;
} catch (error) {
return [];
}
}
// Check if post is already tracked (checks all URL candidates to avoid duplicates)
async function checkPostStatus(postUrl, allUrlCandidates = []) {
try {
@@ -507,13 +550,30 @@ async function checkPostStatus(postUrl, allUrlCandidates = []) {
}
}
console.log('[FB Tracker] Checking post status for URLs:', urlsToCheck);
const photoHostVariants = [];
for (const candidateUrl of urlsToCheck) {
const variants = expandPhotoUrlHostVariants(candidateUrl);
for (const variant of variants) {
if (!urlsToCheck.includes(variant) && !photoHostVariants.includes(variant)) {
photoHostVariants.push(variant);
}
}
}
const allUrlsToCheck = photoHostVariants.length
? urlsToCheck.concat(photoHostVariants)
: urlsToCheck;
if (photoHostVariants.length) {
console.log('[FB Tracker] Added photo host variants for status check:', photoHostVariants);
}
console.log('[FB Tracker] Checking post status for URLs:', allUrlsToCheck);
let foundPost = null;
let foundUrl = null;
// Check each URL
for (const url of urlsToCheck) {
for (const url of allUrlsToCheck) {
const response = await backendFetch(`${API_URL}/posts/by-url?url=${encodeURIComponent(url)}`);
if (response.ok) {
@@ -544,10 +604,14 @@ async function checkPostStatus(postUrl, allUrlCandidates = []) {
}
if (foundPost) {
const urlsForPersistence = allUrlsToCheck.filter(urlValue => urlValue && urlValue !== foundPost.url);
if (urlsForPersistence.length) {
await persistAlternatePostUrls(foundPost.id, urlsForPersistence);
}
return foundPost;
}
console.log('[FB Tracker] Post not tracked yet (checked', urlsToCheck.length, 'URLs)');
console.log('[FB Tracker] Post not tracked yet (checked', allUrlsToCheck.length, 'URLs)');
return null;
} catch (error) {
console.error('[FB Tracker] Error checking post status:', error);
@@ -615,6 +679,29 @@ async function updatePostUrl(postId, newUrl) {
}
}
async function persistAlternatePostUrls(postId, urls = []) {
if (!postId || !Array.isArray(urls) || urls.length === 0) {
return;
}
const uniqueUrls = Array.from(new Set(urls.filter(url => typeof url === 'string' && url.trim())));
if (!uniqueUrls.length) {
return;
}
try {
await backendFetch(`${API_URL}/posts/${postId}/urls`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ urls: uniqueUrls })
});
} catch (error) {
console.debug('[FB Tracker] Persisting alternate URLs failed:', error);
}
}
// Add post to tracking
async function markPostChecked(postId, profileNumber) {
try {
@@ -663,6 +750,8 @@ async function addPostToTracking(postUrl, targetCount, profileNumber, options =
}
}
const alternateCandidates = Array.isArray(options && options.candidates) ? options.candidates : [];
const normalizedUrl = normalizeFacebookPostUrl(postUrl);
if (!normalizedUrl) {
console.error('[FB Tracker] Post URL konnte nicht normalisiert werden:', postUrl);
@@ -676,6 +765,10 @@ async function addPostToTracking(postUrl, targetCount, profileNumber, options =
created_by_profile: profileNumber
};
if (alternateCandidates.length) {
payload.alternate_urls = alternateCandidates;
}
if (createdByName) {
payload.created_by_name = createdByName;
}
@@ -697,11 +790,7 @@ async function addPostToTracking(postUrl, targetCount, profileNumber, options =
console.log('[FB Tracker] Post added successfully:', data);
if (data && data.id) {
const checkedData = await markPostChecked(data.id, profileNumber);
await captureAndUploadScreenshot(data.id, options.postElement || null);
if (checkedData) {
return checkedData;
}
}
return data;
@@ -1690,6 +1779,7 @@ function normalizeFacebookPostUrl(rawValue) {
const cleanedParams = new URLSearchParams();
parsed.searchParams.forEach((paramValue, paramKey) => {
const lowerKey = paramKey.toLowerCase();
const isSingleUnitParam = lowerKey === 's' && paramValue === 'single_unit';
if (
lowerKey.startsWith('__cft__')
|| lowerKey.startsWith('__tn__')
@@ -1698,6 +1788,7 @@ function normalizeFacebookPostUrl(rawValue) {
|| lowerKey === 'set'
|| lowerKey === 'comment_id'
|| lowerKey === 'hoisted_section_header_type'
|| isSingleUnitParam
) {
return;
}
@@ -1721,6 +1812,127 @@ function normalizeFacebookPostUrl(rawValue) {
return formatted.replace(/[?&]$/, '');
}
async function renderTrackedStatus({
container,
postElement,
postData,
profileNumber,
isFeedHome,
isDialogContext,
manualHideInfo,
encodedUrl,
postNum
}) {
if (!postData) {
container.innerHTML = '';
return { hidden: false };
}
if (postData.id) {
container.dataset.postId = postData.id;
}
const checks = Array.isArray(postData.checks) ? postData.checks : [];
const checkedCount = postData.checked_count ?? checks.length;
const targetTotal = postData.target_count || checks.length || 0;
const statusText = `${checkedCount}/${targetTotal}`;
const completed = checkedCount >= targetTotal && targetTotal > 0;
const lastCheck = checks.length
? new Date(checks[checks.length - 1].checked_at).toLocaleString('de-DE', { timeZone: 'Europe/Berlin' })
: null;
const isExpired = postData.deadline_at ? new Date(postData.deadline_at) < new Date() : false;
const canCurrentProfileCheck = postData.next_required_profile === profileNumber;
const isCurrentProfileDone = checks.some(check => check.profile_number === profileNumber);
if (isFeedHome && isCurrentProfileDone) {
if (isDialogContext) {
console.log('[FB Tracker] Post #' + postNum + ' - Would hide on feed (already checked by current profile) but skipping in dialog context');
} else {
console.log('[FB Tracker] Post #' + postNum + ' - Hidden on feed (already checked by current profile)');
hidePostElement(postElement);
processedPostUrls.set(encodedUrl, {
element: postElement,
createdAt: Date.now(),
hidden: true,
searchSeenCount: manualHideInfo && typeof manualHideInfo.seen_count === 'number'
? manualHideInfo.seen_count
: null
});
return { hidden: true };
}
}
const expiredText = isExpired ? ' ⚠️ ABGELAUFEN' : '';
let statusHtml = `
<div style="color: #65676b; white-space: nowrap;">
<strong>Tracker:</strong> ${statusText}${completed ? ' ✓' : ''}${expiredText}
</div>
${lastCheck ? `<div style="color: #65676b; font-size: 12px; white-space: nowrap;">Letzte: ${lastCheck}</div>` : ''}
`;
if (canCurrentProfileCheck && !isExpired && !completed) {
statusHtml += `
<button class="fb-tracker-check-btn" style="
padding: 4px 12px;
background-color: #42b72a;
color: white;
border: none;
border-radius: 4px;
font-weight: 600;
cursor: pointer;
font-size: 13px;
white-space: nowrap;
margin-left: 8px;
">
✓ Bestätigen
</button>
`;
} else if (isCurrentProfileDone) {
statusHtml += `
<div style="color: #42b72a; font-size: 12px; white-space: nowrap; margin-left: 8px;">
✓ Von dir bestätigt
</div>
`;
}
container.innerHTML = statusHtml;
await addAICommentButton(container, postElement);
const checkBtn = container.querySelector('.fb-tracker-check-btn');
if (checkBtn) {
checkBtn.addEventListener('click', async () => {
checkBtn.disabled = true;
checkBtn.textContent = 'Wird bestätigt...';
const result = await markPostChecked(postData.id, profileNumber);
if (result) {
await renderTrackedStatus({
container,
postElement,
postData: result,
profileNumber,
isFeedHome,
isDialogContext,
manualHideInfo,
encodedUrl,
postNum
});
} else {
checkBtn.disabled = false;
checkBtn.textContent = 'Fehler - Erneut versuchen';
checkBtn.style.backgroundColor = '#e74c3c';
}
});
}
console.log('[FB Tracker] Showing status:', statusText);
return { hidden: false };
}
// Create the tracking UI
async function createTrackerUI(postElement, buttonBar, postNum = '?', options = {}) {
// Normalize to top-level post container if nested element passed
@@ -1789,6 +2001,8 @@ async function createTrackerUI(postElement, buttonBar, postNum = '?', options =
container.id = 'fb-tracker-ui-post-' + postNum;
container.setAttribute('data-post-num', postNum);
container.setAttribute('data-post-url', encodedUrl);
container.dataset.isFeedHome = isFeedHome ? '1' : '0';
container.dataset.isDialogContext = isDialogContext ? '1' : '0';
container.style.cssText = `
padding: 6px 12px;
background-color: #f0f2f5;
@@ -1894,111 +2108,21 @@ async function createTrackerUI(postElement, buttonBar, postNum = '?', options =
}
if (postData) {
const checkedCount = postData.checked_count ?? (Array.isArray(postData.checks) ? postData.checks.length : 0);
const statusText = `${checkedCount}/${postData.target_count}`;
const completed = checkedCount >= postData.target_count;
const lastCheck = Array.isArray(postData.checks) && postData.checks.length
? new Date(postData.checks[postData.checks.length - 1].checked_at).toLocaleString('de-DE', { timeZone: 'Europe/Berlin' })
: null;
const renderResult = await renderTrackedStatus({
container,
postElement,
postData,
profileNumber,
isFeedHome,
isDialogContext,
manualHideInfo,
encodedUrl,
postNum
});
// Check if deadline has passed
const isExpired = postData.deadline_at ? new Date(postData.deadline_at) < new Date() : false;
const expiredText = isExpired ? ' ⚠️ ABGELAUFEN' : '';
// Check if current profile can check this post
const canCurrentProfileCheck = postData.next_required_profile === profileNumber;
const isCurrentProfileDone = Array.isArray(postData.checks) && postData.checks.some(check => check.profile_number === profileNumber);
if (isFeedHome && isCurrentProfileDone) {
if (isDialogContext) {
console.log('[FB Tracker] Post #' + postNum + ' - Would hide on feed (already checked by current profile) but skipping in dialog context');
} else {
console.log('[FB Tracker] Post #' + postNum + ' - Hidden on feed (already checked by current profile)');
hidePostElement(postElement);
processedPostUrls.set(encodedUrl, {
element: postElement,
createdAt: Date.now(),
hidden: true,
searchSeenCount: manualHideInfo && typeof manualHideInfo.seen_count === 'number' ? manualHideInfo.seen_count : null
});
return;
}
if (renderResult && renderResult.hidden) {
return;
}
let statusHtml = `
<div style="color: #65676b; white-space: nowrap;">
<strong>Tracker:</strong> ${statusText}${completed ? ' ✓' : ''}${expiredText}
</div>
${lastCheck ? `<div style="color: #65676b; font-size: 12px; white-space: nowrap;">Letzte: ${lastCheck}</div>` : ''}
`;
// Add check button if current profile can check and not expired
if (canCurrentProfileCheck && !isExpired && !completed) {
statusHtml += `
<button class="fb-tracker-check-btn" style="
padding: 4px 12px;
background-color: #42b72a;
color: white;
border: none;
border-radius: 4px;
font-weight: 600;
cursor: pointer;
font-size: 13px;
white-space: nowrap;
margin-left: 8px;
">
✓ Bestätigen
</button>
`;
} else if (isCurrentProfileDone) {
statusHtml += `
<div style="color: #42b72a; font-size: 12px; white-space: nowrap; margin-left: 8px;">
✓ Von dir bestätigt
</div>
`;
}
container.innerHTML = statusHtml;
// Add AI button
await addAICommentButton(container, postElement);
// Add event listener for check button
const checkBtn = container.querySelector('.fb-tracker-check-btn');
if (checkBtn) {
checkBtn.addEventListener('click', async () => {
checkBtn.disabled = true;
checkBtn.textContent = 'Wird bestätigt...';
const result = await markPostChecked(postData.id, profileNumber);
if (result) {
const newCheckedCount = result.checked_count ?? checkedCount + 1;
const newStatusText = `${newCheckedCount}/${postData.target_count}`;
const newCompleted = newCheckedCount >= postData.target_count;
const newLastCheck = new Date().toLocaleString('de-DE', { timeZone: 'Europe/Berlin' });
container.innerHTML = `
<div style="color: #65676b; white-space: nowrap;">
<strong>Tracker:</strong> ${newStatusText}${newCompleted ? ' ✓' : ''}
</div>
<div style="color: #65676b; font-size: 12px; white-space: nowrap;">Letzte: ${newLastCheck}</div>
<div style="color: #42b72a; font-size: 12px; white-space: nowrap; margin-left: 8px;">
✓ Von dir bestätigt
</div>
`;
// Re-add AI button after update
await addAICommentButton(container, postElement);
} else {
checkBtn.disabled = false;
checkBtn.textContent = 'Fehler - Erneut versuchen';
checkBtn.style.backgroundColor = '#e74c3c';
}
});
}
console.log('[FB Tracker] Showing status:', statusText);
} else {
// Post not tracked - show add UI
const selectId = `tracker-select-${Date.now()}`;
@@ -2072,37 +2196,28 @@ async function createTrackerUI(postElement, buttonBar, postNum = '?', options =
const result = await addPostToTracking(postUrlData.url, targetCount, profileNumber, {
postElement,
deadline: deadlineValue
deadline: deadlineValue,
candidates: postUrlData.allCandidates
});
if (result) {
const checks = Array.isArray(result.checks) ? result.checks : [];
const checkedCount = typeof result.checked_count === 'number' ? result.checked_count : checks.length;
const targetTotal = result.target_count || targetCount;
const statusText = `${checkedCount}/${targetTotal}`;
const completed = checkedCount >= targetTotal;
const lastCheck = checks.length ? new Date(checks[checks.length - 1].checked_at).toLocaleString('de-DE', { timeZone: 'Europe/Berlin' }) : null;
const renderOutcome = await renderTrackedStatus({
container,
postElement,
postData: result,
profileNumber,
isFeedHome,
isDialogContext,
manualHideInfo,
encodedUrl,
postNum
});
let statusHtml = `
<div style="color: #65676b; white-space: nowrap;">
<strong>Tracker:</strong> ${statusText}${completed ? ' ✓' : ''}
</div>
`;
if (lastCheck) {
statusHtml += `
<div style="color: #65676b; font-size: 12px; white-space: nowrap;">
Letzte: ${lastCheck}
</div>
`;
if (renderOutcome && renderOutcome.hidden) {
return;
}
container.innerHTML = statusHtml;
if (deadlineInput) {
deadlineInput.value = '';
}
await addAICommentButton(container, postElement);
return;
} else {
// Error
addButton.disabled = false;
@@ -3667,6 +3782,77 @@ async function addAICommentButton(container, postElement) {
window.removeEventListener('resize', repositionDropdown);
};
const getDecodedPostUrl = () => {
const raw = encodedPostUrl || (container && container.getAttribute('data-post-url'));
if (!raw) {
return null;
}
try {
return decodeURIComponent(raw);
} catch (error) {
console.warn('[FB Tracker] Konnte Post-URL nicht dekodieren:', error);
return null;
}
};
const confirmParticipationAfterAI = async (profileNumber) => {
try {
if (!container) {
return;
}
const effectiveProfile = profileNumber || await getProfileNumber();
const decodedUrl = getDecodedPostUrl();
const isFeedHomeFlag = container.dataset.isFeedHome === '1';
const isDialogFlag = container.dataset.isDialogContext === '1';
const postNumValue = container.getAttribute('data-post-num') || '?';
const encodedUrlValue = container.getAttribute('data-post-url') || '';
let latestData = null;
let postId = container.dataset.postId || '';
if (postId) {
latestData = await markPostChecked(postId, effectiveProfile);
if (!latestData && decodedUrl) {
const refreshed = await checkPostStatus(decodedUrl);
if (refreshed && refreshed.id) {
container.dataset.postId = refreshed.id;
latestData = await markPostChecked(refreshed.id, effectiveProfile) || refreshed;
}
}
} else if (decodedUrl) {
const refreshed = await checkPostStatus(decodedUrl);
if (refreshed && refreshed.id) {
container.dataset.postId = refreshed.id;
latestData = await markPostChecked(refreshed.id, effectiveProfile) || refreshed;
}
}
if (!latestData && decodedUrl) {
const fallbackStatus = await checkPostStatus(decodedUrl);
if (fallbackStatus) {
latestData = fallbackStatus;
}
}
if (latestData) {
await renderTrackedStatus({
container,
postElement,
postData: latestData,
profileNumber: effectiveProfile,
isFeedHome: isFeedHomeFlag,
isDialogContext: isDialogFlag,
manualHideInfo: null,
encodedUrl: encodedUrlValue,
postNum: postNumValue
});
}
} catch (error) {
console.error('[FB Tracker] Auto-confirm nach AI fehlgeschlagen:', error);
}
};
const handleOutsideClick = (event) => {
if (!wrapper.contains(event.target) && !dropdown.contains(event.target)) {
closeDropdown();
@@ -4134,6 +4320,7 @@ async function addAICommentButton(container, postElement) {
throwIfCancelled();
showToast('📋 Kommentarfeld nicht gefunden - In Zwischenablage kopiert', 'info');
restoreIdle('📋 Kopiert', 2000);
await confirmParticipationAfterAI(profileNumber);
return;
}
@@ -4148,11 +4335,13 @@ async function addAICommentButton(container, postElement) {
if (success) {
showToast('✓ Kommentar wurde eingefügt', 'success');
restoreIdle('✓ Eingefügt', 2000);
await confirmParticipationAfterAI(profileNumber);
} else {
await navigator.clipboard.writeText(comment);
throwIfCancelled();
showToast('📋 Einfügen fehlgeschlagen - In Zwischenablage kopiert', 'info');
restoreIdle('📋 Kopiert', 2000);
await confirmParticipationAfterAI(profileNumber);
}
} catch (error) {
const cancelled = aiContext.cancelled || isCancellationError(error);