// Facebook Post Tracker Extension
// Uses API_BASE_URL from config.js
const EXTENSION_VERSION = '1.2.0';
const PROCESSED_ATTR = 'data-fb-tracker-processed';
const PENDING_ATTR = 'data-fb-tracker-pending';
const DIALOG_ROOT_SELECTOR = '[role="dialog"], [data-pagelet*="Modal"], [data-pagelet="StoriesRecentStoriesFeedSection"]';
const API_URL = `${API_BASE_URL}/api`;
const WEBAPP_BASE_URL = API_BASE_URL.replace(/\/+$/, '');
const MAX_SELECTION_LENGTH = 5000;
const postSelectionCache = new WeakMap();
const LAST_SELECTION_MAX_AGE = 5000;
let selectionCacheTimeout = null;
let lastGlobalSelection = { text: '', timestamp: 0 };
const processedPostUrls = new Map();
const SEARCH_RESULTS_PATH_PREFIX = '/search';
const FEED_HOME_PATHS = ['/', '/home.php'];
const sessionSearchRecordedUrls = new Set();
const sessionSearchInfoCache = new Map();
function isOnSearchResultsPage() {
try {
const pathname = window.location && window.location.pathname;
return typeof pathname === 'string' && pathname.startsWith(SEARCH_RESULTS_PATH_PREFIX);
} catch (error) {
return false;
}
}
const trackerElementsByPost = new WeakMap();
const postAdditionalNotes = new WeakMap();
const REELS_PATH_PREFIX = '/reel/';
const POST_TEXT_LOG_TAG = '[FB PostText]';
function isOnReelsPage() {
try {
const pathname = window.location && window.location.pathname;
return typeof pathname === 'string' && pathname.startsWith(REELS_PATH_PREFIX);
} catch (error) {
return false;
}
}
let debugLoggingEnabled = false;
const originalConsoleLog = console.log.bind(console);
const originalConsoleDebug = console.debug ? console.debug.bind(console) : null;
const originalConsoleInfo = console.info ? console.info.bind(console) : null;
function shouldSuppressTrackerLog(args) {
if (!args || args.length === 0) {
return false;
}
const [first] = args;
if (typeof first === 'string' && first.startsWith('[FB Tracker]')) {
return !debugLoggingEnabled;
}
return false;
}
console.log = (...args) => {
if (shouldSuppressTrackerLog(args)) {
return;
}
originalConsoleLog(...args);
};
if (originalConsoleDebug) {
console.debug = (...args) => {
if (shouldSuppressTrackerLog(args)) {
return;
}
originalConsoleDebug(...args);
};
}
if (originalConsoleInfo) {
console.info = (...args) => {
if (shouldSuppressTrackerLog(args)) {
return;
}
originalConsoleInfo(...args);
};
}
function applyDebugLoggingPreference(value) {
debugLoggingEnabled = Boolean(value);
if (debugLoggingEnabled) {
originalConsoleLog('[FB Tracker] Debug logging enabled');
}
}
chrome.storage.sync.get(['debugLoggingEnabled'], (result) => {
applyDebugLoggingPreference(result && typeof result.debugLoggingEnabled !== 'undefined'
? result.debugLoggingEnabled
: false);
});
chrome.storage.onChanged.addListener((changes, area) => {
if (area === 'sync' && changes && Object.prototype.hasOwnProperty.call(changes, 'debugLoggingEnabled')) {
applyDebugLoggingPreference(changes.debugLoggingEnabled.newValue);
}
});
function getTrackerElementForPost(postElement) {
if (!postElement) {
return null;
}
const tracker = trackerElementsByPost.get(postElement);
if (tracker && tracker.isConnected) {
return tracker;
}
if (tracker) {
trackerElementsByPost.delete(postElement);
}
return null;
}
function setTrackerElementForPost(postElement, trackerElement) {
if (!postElement) {
return;
}
if (trackerElement && trackerElement.isConnected) {
trackerElementsByPost.set(postElement, trackerElement);
} else {
trackerElementsByPost.delete(postElement);
}
}
function clearTrackerElementForPost(postElement, trackerElement = null) {
if (!postElement) {
return;
}
if (!trackerElementsByPost.has(postElement)) {
return;
}
const current = trackerElementsByPost.get(postElement);
if (trackerElement && current && current !== trackerElement) {
return;
}
trackerElementsByPost.delete(postElement);
}
const AI_CREDENTIAL_CACHE_TTL = 60 * 1000; // 1 minute cache
const aiCredentialCache = {
data: null,
timestamp: 0,
pending: null
};
const MODERATION_SETTINGS_CACHE_TTL = 5 * 60 * 1000;
const moderationSettingsCache = {
data: null,
timestamp: 0,
pending: null
};
const SPORTS_SCORING_DEFAULTS = {
threshold: 5,
weights: {
scoreline: 3,
scoreEmoji: 2,
sportEmoji: 2,
sportVerb: 1.5,
sportNoun: 2,
hashtag: 1.5,
teamToken: 2,
competition: 2,
celebration: 1,
location: 1
}
};
const DEFAULT_SPORT_TERMS = {
nouns: [
'auswärtssieg', 'heimsieg', 'derbysieg', 'revanche', 'spiel', 'spieltag', 'match', 'derby', 'finale',
'cup', 'pokal', 'liga', 'bundesliga', 'oberliga', 'kreisliga', 'bezirksliga', 'meisterschaft',
'turnier', 'halbzeit', 'tabellenplatz', 'tabelle', 'tor', 'tore', 'treffer', 'stadion', 'arena', 'halle',
'trainerteam', 'mannschaft', 'fans', 'fanblock', 'jugend', 'u17', 'u19', 'u15'
],
verbs: [
'gewinnen', 'siegen', 'geholt', 'erkämpfen', 'erkämpft', 'erkämpfen', 'drehen', 'punkten',
'trifft', 'treffen', 'schießt', 'schiesst', 'schießen', 'schiessen', 'verteidigen', 'stürmen', 'kämpfen'
],
competitions: [
'bundesliga', 'liga', 'serie a', 'premier league', 'champions league', 'europa league', 'dfb-pokal',
'cup', 'pokal', 'qualifikation', 'qualirunde', 'halbfinale', 'viertelfinale', 'achtelfinale', 'relegation'
],
celebrations: [
'sieg', 'siege', 'auswärtssieg', 'heimsieg', 'auswärtsspiel', 'punkte', 'punkte geholt', 'man of the match', 'motm',
'tabellenführung', 'tabellenplatz', 'tabellendritter', 'tabellenzweiter'
],
locations: [
'auswärts', 'heimspiel', 'derby', 'arena', 'stadion', 'halle', 'bolle', 'bölle', 'mosel'
],
negatives: [
'rezept', 'kochen', 'politik', 'wahl', 'bundestag', 'landtag', 'software', 'release', 'update', 'konzert',
'album', 'tour', 'podcast', 'karriere', 'job', 'stellenangebot', 'bewerbung'
]
};
console.log(`[FB Tracker v${EXTENSION_VERSION}] Extension loaded, API URL:`, API_URL);
function ensureTrackerActionsContainer(container) {
if (!container) {
return null;
}
let actionsContainer = container.querySelector('.fb-tracker-actions-end');
if (actionsContainer && actionsContainer.isConnected) {
return actionsContainer;
}
actionsContainer = document.createElement('div');
actionsContainer.className = 'fb-tracker-actions-end';
actionsContainer.style.cssText = `
margin-left: auto;
display: inline-flex;
align-items: center;
gap: 8px;
`;
container.appendChild(actionsContainer);
return actionsContainer;
}
function backendFetch(url, options = {}) {
const config = {
...options,
credentials: 'include'
};
if (options && options.headers) {
config.headers = { ...options.headers };
}
return fetch(url, config);
}
async function fetchActiveAICredentials(forceRefresh = false) {
const now = Date.now();
if (!forceRefresh && aiCredentialCache.data && (now - aiCredentialCache.timestamp < AI_CREDENTIAL_CACHE_TTL)) {
return aiCredentialCache.data;
}
if (aiCredentialCache.pending) {
try {
return await aiCredentialCache.pending;
} catch (error) {
// fall through to retry below
}
}
aiCredentialCache.pending = (async () => {
const response = await backendFetch(`${API_URL}/ai-credentials`);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.error || 'AI-Provider konnten nicht geladen werden');
}
const credentials = await response.json();
let active = Array.isArray(credentials)
? credentials.filter(entry => entry && Number(entry.is_active) === 1)
: [];
if (active.length === 0 && Array.isArray(credentials)) {
active = credentials.slice();
}
aiCredentialCache.data = active;
aiCredentialCache.timestamp = Date.now();
return active;
})();
try {
return await aiCredentialCache.pending;
} finally {
aiCredentialCache.pending = null;
}
}
function normalizeModerationSettings(payload) {
if (!payload || typeof payload !== 'object') {
return {
sports_scoring_enabled: true,
sports_score_threshold: SPORTS_SCORING_DEFAULTS.threshold,
sports_score_weights: SPORTS_SCORING_DEFAULTS.weights,
sports_terms: DEFAULT_SPORT_TERMS,
sports_auto_hide_enabled: false
};
}
const threshold = (() => {
const parsed = parseFloat(payload.sports_score_threshold);
if (Number.isNaN(parsed) || parsed < 0) {
return SPORTS_SCORING_DEFAULTS.threshold;
}
return Math.min(50, Math.max(0, parsed));
})();
const weightsSource = payload.sports_score_weights && typeof payload.sports_score_weights === 'object'
? payload.sports_score_weights
: {};
const normalizedWeights = { ...SPORTS_SCORING_DEFAULTS.weights };
for (const key of Object.keys(SPORTS_SCORING_DEFAULTS.weights)) {
const raw = weightsSource[key];
const parsed = typeof raw === 'number' ? raw : parseFloat(raw);
if (Number.isFinite(parsed)) {
normalizedWeights[key] = Math.max(0, Math.min(10, parsed));
}
}
const normalizeTerms = (terms) => {
const base = { ...DEFAULT_SPORT_TERMS };
const src = terms && typeof terms === 'object' ? terms : {};
const normalizeList = (list, fallback) => {
if (!Array.isArray(list)) return fallback;
const cleaned = list
.map((entry) => typeof entry === 'string' ? entry.trim().toLowerCase() : '')
.filter((entry) => entry);
const unique = Array.from(new Set(cleaned)).slice(0, 200);
return unique.length ? unique : fallback;
};
return {
nouns: normalizeList(src.nouns, base.nouns),
verbs: normalizeList(src.verbs, base.verbs),
competitions: normalizeList(src.competitions, base.competitions),
celebrations: normalizeList(src.celebrations, base.celebrations),
locations: normalizeList(src.locations, base.locations),
negatives: normalizeList(src.negatives, base.negatives)
};
};
return {
sports_scoring_enabled: payload.sports_scoring_enabled !== false,
sports_score_threshold: threshold,
sports_score_weights: normalizedWeights,
sports_terms: normalizeTerms(payload.sports_terms),
sports_auto_hide_enabled: !!payload.sports_auto_hide_enabled
};
}
async function fetchModerationSettings(forceRefresh = false) {
const now = Date.now();
if (!forceRefresh && moderationSettingsCache.data && (now - moderationSettingsCache.timestamp < MODERATION_SETTINGS_CACHE_TTL)) {
return moderationSettingsCache.data;
}
if (moderationSettingsCache.pending) {
try {
return await moderationSettingsCache.pending;
} catch (error) {
// fallthrough to retry
}
}
moderationSettingsCache.pending = (async () => {
const response = await backendFetch(`${API_URL}/moderation-settings`);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.error || 'Moderations-Einstellungen konnten nicht geladen werden');
}
const data = await response.json();
const normalized = normalizeModerationSettings(data);
moderationSettingsCache.data = normalized;
moderationSettingsCache.timestamp = Date.now();
return normalized;
})();
try {
return await moderationSettingsCache.pending;
} finally {
moderationSettingsCache.pending = null;
}
}
function formatAICredentialLabel(credential) {
if (!credential || typeof credential !== 'object') {
return 'Unbekannte AI';
}
const name = (credential.name || '').trim();
const provider = (credential.provider || '').trim();
const model = (credential.model || '').trim();
if (name) {
if (provider && model) {
return `${name} · ${provider}/${model}`;
}
if (provider) {
return `${name} · ${provider}`;
}
if (model) {
return `${name} · ${model}`;
}
return name;
}
if (provider && model) {
return `${provider}/${model}`;
}
if (model) {
return model;
}
if (provider) {
return provider;
}
return 'AI Provider';
}
document.addEventListener('selectionchange', () => {
if (selectionCacheTimeout) {
clearTimeout(selectionCacheTimeout);
}
selectionCacheTimeout = setTimeout(() => {
cacheCurrentSelection();
selectionCacheTimeout = null;
}, 50);
});
// Profile state helpers
async function fetchBackendProfileNumber() {
try {
const response = await backendFetch(`${API_URL}/profile-state`);
if (!response.ok) {
return null;
}
const data = await response.json();
if (data && data.profile_number) {
return data.profile_number;
}
} catch (error) {
console.warn('[FB Tracker] Failed to fetch profile state from backend:', error);
}
return null;
}
function extractAuthorName(postElement) {
if (!postElement) {
return null;
}
const selectors = [
'h2 strong a[role="link"]',
'h2 span a[role="link"]',
'a[role="link"][tabindex="0"] strong',
'a[role="link"][tabindex="0"]'
];
for (const selector of selectors) {
const node = postElement.querySelector(selector);
if (node && node.textContent) {
const text = node.textContent.trim();
if (text) {
return text;
}
}
}
const ariaLabelNode = postElement.querySelector('[aria-label]');
if (ariaLabelNode) {
const ariaLabel = ariaLabelNode.getAttribute('aria-label');
if (ariaLabel && ariaLabel.trim()) {
return ariaLabel.trim();
}
}
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;
}
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);
});
});
}
// Extract post URL from post element
function cleanPostUrl(rawUrl) {
if (!rawUrl) {
return '';
}
const cftIndex = rawUrl.indexOf('__cft__');
let trimmed = cftIndex !== -1 ? rawUrl.slice(0, cftIndex) : rawUrl;
trimmed = trimmed.replace(/[?&]$/, '');
return trimmed;
}
function toAbsoluteFacebookUrl(rawUrl) {
if (!rawUrl) {
return null;
}
const cleaned = cleanPostUrl(rawUrl);
let url;
try {
url = new URL(cleaned);
} catch (error) {
try {
url = new URL(cleaned, window.location.origin);
} catch (innerError) {
return null;
}
}
const host = url.hostname.toLowerCase();
if (!host.endsWith('facebook.com')) {
return null;
}
return url;
}
function isValidFacebookPostUrl(url) {
if (!url) {
return false;
}
const path = url.pathname.toLowerCase();
const searchParams = url.searchParams;
const postPathPatterns = [
'/posts/',
'/permalink/',
'/photos/',
'/videos/',
'/reel/',
'/watch/'
];
if (postPathPatterns.some(pattern => path.includes(pattern))) {
return true;
}
if (path.startsWith('/photo') && searchParams.has('fbid')) {
return true;
}
if (path.startsWith('/photo.php') && searchParams.has('fbid')) {
return true;
}
if (path === '/permalink.php' && searchParams.has('story_fbid')) {
return true;
}
if (path.startsWith('/groups/') && searchParams.has('multi_permalinks')) {
return true;
}
// /watch/ URLs with video ID parameter
if (path.startsWith('/watch') && searchParams.has('v')) {
return true;
}
return false;
}
function formatFacebookPostUrl(url) {
if (!url) {
return '';
}
let search = url.search;
if (search.endsWith('?') || search.endsWith('&')) {
search = search.slice(0, -1);
}
return `${url.origin}${url.pathname}${search}`;
}
function extractPostUrlCandidate(rawUrl) {
const absoluteUrl = toAbsoluteFacebookUrl(rawUrl);
if (!absoluteUrl || !isValidFacebookPostUrl(absoluteUrl)) {
return '';
}
const formatted = formatFacebookPostUrl(absoluteUrl);
return normalizeFacebookPostUrl(formatted);
}
function getPostUrl(postElement, postNum = '?') {
console.log('[FB Tracker] Post #' + postNum + ' - Extracting URL from:', postElement);
const allCandidates = [];
// Strategy 1: Look for attribution links inside post
const attributionLinks = postElement.querySelectorAll('a[attributionsrc*="/privacy_sandbox/comet/"][href]');
for (const link of attributionLinks) {
const candidate = extractPostUrlCandidate(link.href);
if (candidate && !allCandidates.includes(candidate)) {
allCandidates.push(candidate);
}
}
// Strategy 2: Look in parent elements (timestamp might be outside container)
let current = postElement;
for (let i = 0; i < 5 && current; i++) {
const parentLinks = current.querySelectorAll('a[attributionsrc*="/privacy_sandbox/comet/"][href]');
for (const link of parentLinks) {
const candidate = extractPostUrlCandidate(link.href);
if (candidate && !allCandidates.includes(candidate)) {
allCandidates.push(candidate);
}
}
current = current.parentElement;
}
// Strategy 3: Check all links in post
const links = postElement.querySelectorAll('a[href]');
for (const link of links) {
const candidate = extractPostUrlCandidate(link.href);
if (candidate && !allCandidates.includes(candidate)) {
allCandidates.push(candidate);
}
}
// Prefer main post links over photo/video links
const mainPostLink = allCandidates.find(url =>
url.includes('/posts/') || url.includes('/permalink/') || url.includes('/permalink.php')
);
if (mainPostLink) {
console.log('[FB Tracker] Post #' + postNum + ' - Found main post URL:', mainPostLink, postElement);
return { url: mainPostLink, allCandidates };
}
// Fallback to first candidate
if (allCandidates.length > 0) {
console.log('[FB Tracker] Post #' + postNum + ' - Using first candidate URL:', allCandidates[0], postElement);
return { url: allCandidates[0], allCandidates };
}
const fallbackCandidate = extractPostUrlCandidate(window.location.href);
if (fallbackCandidate) {
console.log('[FB Tracker] Post #' + postNum + ' - Using fallback URL:', fallbackCandidate, postElement);
return { url: fallbackCandidate, allCandidates: [fallbackCandidate] };
}
console.log('[FB Tracker] Post #' + postNum + ' - No valid URL found for:', postElement);
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 {
const normalizedUrl = normalizeFacebookPostUrl(postUrl);
if (!normalizedUrl) {
console.warn('[FB Tracker] Überspringe Statusabfrage, URL ungültig:', postUrl);
return null;
}
// Build list of URLs to check (primary + all candidates)
const urlsToCheck = [normalizedUrl];
console.log('[FB Tracker] Received candidates to check:', allUrlCandidates);
for (const candidate of allUrlCandidates) {
const normalized = normalizeFacebookPostUrl(candidate);
if (normalized && !urlsToCheck.includes(normalized)) {
urlsToCheck.push(normalized);
}
}
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 allUrlsToCheck) {
const response = await backendFetch(`${API_URL}/posts/by-url?url=${encodeURIComponent(url)}`);
if (response.ok) {
const data = await response.json();
if (data && data.id) {
console.log('[FB Tracker] Post found with URL:', url, data);
foundPost = data;
foundUrl = url;
break;
} else {
console.log('[FB Tracker] URL not found in backend:', url);
}
} else {
console.log('[FB Tracker] Backend error for URL:', url, response.status);
}
}
// If post found and we have a better main post URL, update it
if (foundPost && foundUrl !== normalizedUrl) {
const isMainPostUrl = normalizedUrl.includes('/posts/') || normalizedUrl.includes('/permalink/');
const isPhotoUrl = foundUrl.includes('/photo');
if (isMainPostUrl && isPhotoUrl) {
console.log('[FB Tracker] Updating post URL from photo link to main post link:', foundUrl, '->', normalizedUrl);
await updatePostUrl(foundPost.id, normalizedUrl);
foundPost.url = normalizedUrl; // Update local copy
}
}
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', allUrlsToCheck.length, 'URLs)');
return null;
} catch (error) {
console.error('[FB Tracker] Error checking post status:', error);
return null;
}
}
async function recordSearchResultPost(primaryUrl, allUrlCandidates = [], options = {}) {
try {
if (!primaryUrl) {
return null;
}
const { skipIncrement = false, forceHide = false, sportsAutoHide = false } = options || {};
const payload = {
url: primaryUrl,
candidates: Array.isArray(allUrlCandidates) ? allUrlCandidates : [],
skip_increment: !!skipIncrement,
force_hide: !!forceHide,
sports_auto_hide: !!sportsAutoHide
};
const response = await backendFetch(`${API_URL}/search-posts`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
});
if (!response.ok) {
console.warn('[FB Tracker] Failed to record search post occurrence:', response.status);
return null;
}
const data = await response.json();
return data;
} catch (error) {
console.error('[FB Tracker] Error recording search result post:', error);
return null;
}
}
// Update post URL
async function updatePostUrl(postId, newUrl) {
try {
const response = await backendFetch(`${API_URL}/posts/${postId}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ url: newUrl })
});
if (response.ok) {
console.log('[FB Tracker] Post URL updated successfully');
return true;
} else {
console.error('[FB Tracker] Failed to update post URL:', response.status);
return false;
}
} catch (error) {
console.error('[FB Tracker] Error updating post URL:', error);
return false;
}
}
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 {
console.log('[FB Tracker] Marking post as checked:', postId, 'Profile:', profileNumber);
const response = await backendFetch(`${API_URL}/posts/${postId}/check`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ profile_number: profileNumber })
});
if (response.ok) {
const data = await response.json();
console.log('[FB Tracker] Post marked as checked:', data);
return data;
}
if (response.status === 409) {
console.log('[FB Tracker] Post already checked by this profile');
return null;
}
console.error('[FB Tracker] Failed to mark post as checked:', response.status);
return null;
} catch (error) {
console.error('[FB Tracker] Error marking post as checked:', error);
return null;
}
}
async function addPostToTracking(postUrl, targetCount, profileNumber, options = {}) {
try {
console.log('[FB Tracker] Adding post:', postUrl, 'Target:', targetCount, 'Profile:', profileNumber);
let createdByName = null;
if (options && options.postElement) {
createdByName = extractAuthorName(options.postElement) || null;
}
let postText = null;
if (options && options.postElement) {
try {
postText = extractPostText(options.postElement) || null;
} catch (error) {
console.debug('[FB Tracker] Failed to extract post text:', error);
}
}
let deadlineIso = null;
if (options && typeof options.deadline === 'string' && options.deadline.trim()) {
const parsedDeadline = new Date(options.deadline.trim());
if (!Number.isNaN(parsedDeadline.getTime())) {
deadlineIso = parsedDeadline.toISOString();
}
}
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);
return null;
}
const payload = {
url: normalizedUrl,
target_count: targetCount,
profile_number: profileNumber,
created_by_profile: profileNumber
};
if (alternateCandidates.length) {
payload.alternate_urls = alternateCandidates;
}
if (createdByName) {
payload.created_by_name = createdByName;
}
if (deadlineIso) {
payload.deadline_at = deadlineIso;
}
if (postText) {
payload.post_text = postText;
}
const response = await backendFetch(`${API_URL}/posts`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(payload)
});
if (response.ok) {
const data = await response.json();
console.log('[FB Tracker] Post added successfully:', data);
if (data && data.id) {
await captureAndUploadScreenshot(data.id, options.postElement || null);
}
return data;
} else {
console.error('[FB Tracker] Failed to add post:', response.status);
return null;
}
} catch (error) {
console.error('[FB Tracker] Error adding post:', error);
return null;
}
}
function normalizeButtonLabel(button) {
const aria = button.getAttribute('aria-label');
if (aria) {
return aria.trim().toLowerCase();
}
const title = button.getAttribute('title');
if (title) {
return title.trim().toLowerCase();
}
return (button.textContent || '').trim().toLowerCase();
}
const LIKE_LABEL_KEYWORDS = ['gefällt mir', 'like', 'mag ich'];
const COMMENT_LABEL_KEYWORDS = ['kommentieren', 'comment', 'kommentar', 'antwort hinterlassen'];
const SHARE_LABEL_KEYWORDS = ['teilen', 'share', 'senden', 'posten', 'profil posten'];
const REPLY_LABEL_KEYWORDS = ['antworten', 'reply'];
const LIKE_ROLE_KEYWORDS = ['gefällt mir', 'like'];
const COMMENT_ROLE_KEYWORDS = ['comment'];
const SHARE_ROLE_KEYWORDS = ['share'];
const REPLY_ROLE_KEYWORDS = ['reply'];
function matchesKeyword(label, keywords) {
return keywords.some((keyword) => label.includes(keyword));
}
function styleIndicatesLiked(styleValue) {
if (!styleValue || typeof styleValue !== 'string') {
return false;
}
const normalized = styleValue.toLowerCase();
return (
normalized.includes('reaction-like')
|| normalized.includes('#0866ff')
|| normalized.includes('rgb(8, 102, 255)')
|| normalized.includes('--reaction-like')
);
}
function elementIndicatesLiked(element) {
if (!element) {
return false;
}
const inlineStyle = (element.getAttribute('style') || '').trim();
if (styleIndicatesLiked(inlineStyle)) {
return true;
}
try {
const computed = window.getComputedStyle(element);
if (computed && computed.color && styleIndicatesLiked(computed.color)) {
return true;
}
} catch (error) {
console.debug('[FB Tracker] Unable to inspect computed style:', error);
}
return false;
}
function isPostLikedByCurrentUser(likeButton, postElement) {
const candidates = [];
if (likeButton) {
candidates.push(likeButton);
}
if (postElement) {
postElement.querySelectorAll('[data-ad-rendering-role*="gefällt" i], [aria-label*="gefällt" i]').forEach((node) => {
if (node && !candidates.includes(node)) {
candidates.push(node);
}
});
}
for (const candidate of candidates) {
if (!candidate) {
continue;
}
if (elementIndicatesLiked(candidate)) {
return true;
}
const styleTarget = candidate.matches('[data-ad-rendering-role*="gefällt" i]')
? candidate
: candidate.querySelector && candidate.querySelector('[data-ad-rendering-role*="gefällt" i]');
if (styleTarget) {
if (elementIndicatesLiked(styleTarget)) {
return true;
}
}
const styledDescendant = candidate.querySelector && candidate.querySelector('[style*="reaction-like"], [style*="#0866FF"], [style*="rgb(8, 102, 255)"], [style*="--reaction-like"]');
if (styledDescendant && elementIndicatesLiked(styledDescendant)) {
return true;
}
const pressedAncestor = candidate.closest && candidate.closest('[aria-pressed="true"]');
if (pressedAncestor && pressedAncestor !== candidate) {
return true;
}
const ariaPressed = candidate.getAttribute && candidate.getAttribute('aria-pressed');
if (ariaPressed && ariaPressed.toLowerCase() === 'true') {
return true;
}
const ariaLabel = (candidate.getAttribute && candidate.getAttribute('aria-label')) || '';
const buttonText = candidate.textContent || '';
const combined = `${ariaLabel} ${buttonText}`.toLowerCase();
const likedIndicators = ['gefällt dir', 'gefällt dir nicht mehr', 'unlike', 'remove like', 'nicht mehr gefällt'];
if (likedIndicators.some(indicator => combined.includes(indicator))) {
return true;
}
}
return false;
}
function hidePostElement(postElement) {
if (!postElement) {
return;
}
const postContainer = ensurePrimaryPostElement(postElement);
if (postContainer && postContainer.closest(DIALOG_ROOT_SELECTOR)) {
console.log('[FB Tracker] Skipping hide for dialog/modal context');
return;
}
if (postContainer && !isMainPost(postContainer, null)) {
console.log('[FB Tracker] Skipping hide for comment container');
return;
}
const removalSelectors = [
'div[role="complementary"]',
'[role="listitem"][aria-posinset]',
'div[aria-posinset]',
'div[data-ad-comet-feed-verbose-tracking]',
'div[data-ad-preview]',
'article[role="article"]',
'article'
];
let elementToRemove = null;
for (const selector of removalSelectors) {
const candidate = postElement.closest(selector);
if (candidate && candidate !== document.body && candidate !== document.documentElement) {
elementToRemove = candidate;
break;
}
}
if (!elementToRemove) {
elementToRemove = postElement;
}
let removalRoot = elementToRemove;
let parentForTimestampCheck = removalRoot.parentElement;
while (parentForTimestampCheck && parentForTimestampCheck !== document.body && parentForTimestampCheck !== document.documentElement) {
const siblings = Array.from(parentForTimestampCheck.children);
const nonRootSiblings = siblings.filter((sibling) => sibling !== removalRoot);
if (!nonRootSiblings.length) {
break;
}
const hasOnlyTimestampSiblings = nonRootSiblings.every((sibling) => isTimestampArtifactNode(sibling));
if (hasOnlyTimestampSiblings) {
removalRoot = parentForTimestampCheck;
parentForTimestampCheck = removalRoot.parentElement;
continue;
}
const hasTimestampSibling = nonRootSiblings.some((sibling) => isTimestampArtifactNode(sibling));
if (hasTimestampSibling && siblings.length <= 3) {
removalRoot = parentForTimestampCheck;
parentForTimestampCheck = removalRoot.parentElement;
continue;
}
break;
}
let parent = removalRoot.parentElement;
while (parent && parent !== document.body && parent !== document.documentElement) {
if (parent.childElementCount > 1) {
break;
}
if (parent.matches('[role="feed"], [role="main"], [role="region"], [data-pagelet], [data-testid="SEARCH_RESULT_CONTAINER"], #ssrb_feed_start')) {
break;
}
removalRoot = parent;
parent = parent.parentElement;
}
removalRoot.setAttribute('data-fb-tracker-hidden', '1');
const removalParent = removalRoot.parentElement;
if (removalParent) {
removalParent.removeChild(removalRoot);
let current = removalParent;
while (current && current !== document.body && current !== document.documentElement) {
if (current.childElementCount > 0) {
break;
}
const nextParent = current.parentElement;
if (!nextParent) {
break;
}
if (nextParent === document.body || nextParent === document.documentElement) {
current.remove();
break;
}
if (nextParent.childElementCount > 1) {
current.remove();
break;
}
current.remove();
current = nextParent;
}
} else {
removalRoot.style.display = 'none';
}
if (removalParent) {
cleanupDanglingSearchArtifacts(removalParent);
} else {
cleanupDanglingSearchArtifacts(document);
}
}
function collectButtonMeta(button) {
const textParts = [];
const roleParts = [];
const label = normalizeButtonLabel(button);
if (label) {
textParts.push(label);
}
const collectRole = (value) => {
if (!value) {
return;
}
const lower = value.toLowerCase();
roleParts.push(lower);
const tokens = lower.split(/[_\-\s]+/);
tokens.forEach((token) => {
if (token) {
roleParts.push(token);
}
});
};
collectRole(button.getAttribute('data-ad-rendering-role'));
const descendantRoles = button.querySelectorAll('[data-ad-rendering-role]');
descendantRoles.forEach((el) => {
collectRole(el.getAttribute('data-ad-rendering-role'));
if (el.textContent) {
textParts.push(normalizeButtonLabel(el));
}
});
return {
text: textParts.join(' '),
roles: roleParts.join(' ')
};
}
function buttonClassification(button) {
const meta = collectButtonMeta(button);
const text = meta.text;
const roles = meta.roles;
const combined = `${text} ${roles}`;
return {
isLike: matchesKeyword(combined, LIKE_LABEL_KEYWORDS) || matchesKeyword(roles, LIKE_ROLE_KEYWORDS),
isComment: matchesKeyword(combined, COMMENT_LABEL_KEYWORDS) || matchesKeyword(roles, COMMENT_ROLE_KEYWORDS),
isShare: matchesKeyword(combined, SHARE_LABEL_KEYWORDS) || matchesKeyword(roles, SHARE_ROLE_KEYWORDS),
isReply: matchesKeyword(combined, REPLY_LABEL_KEYWORDS) || matchesKeyword(roles, REPLY_ROLE_KEYWORDS)
};
}
function findShareButtonForArticle(article) {
const direct = article.querySelector('[data-ad-rendering-role="share_button"]');
if (direct) {
return direct;
}
const shareButtons = document.querySelectorAll('[data-ad-rendering-role="share_button"]');
for (const button of shareButtons) {
const owningArticle = button.closest('[role="article"]');
if (owningArticle === article) {
return button;
}
}
return null;
}
function hasInteractionButtons(container) {
if (!container) {
return false;
}
const buttons = container.querySelectorAll('[role="button"], button');
if (!buttons.length) {
return false;
}
let hasLike = false;
let hasComment = false;
let hasShare = false;
let hasReply = false;
for (const button of buttons) {
const info = buttonClassification(button);
if (info.isLike) {
hasLike = true;
}
if (info.isComment) {
hasComment = true;
}
if (info.isShare) {
hasShare = true;
}
if (info.isReply) {
hasReply = true;
}
}
if (hasLike || hasComment || hasShare) {
console.log('[FB Tracker] Container analysis', {
tag: container.tagName,
classes: container.className,
hasLike,
hasComment,
hasShare,
hasReply,
buttonCount: buttons.length
});
}
const interactionCount = [hasLike, hasComment, hasShare].filter(Boolean).length;
if (interactionCount === 0) {
return false;
}
if (hasShare) {
return true;
}
if (interactionCount >= 2) {
return true;
}
return !hasReply;
}
function findButtonBar(postElement) {
const shareAnchor = findShareButtonForArticle(postElement);
if (shareAnchor) {
let container = shareAnchor.closest('[role="button"]') || shareAnchor.closest('button');
for (let i = 0; i < 4 && container; i++) {
if (hasInteractionButtons(container)) {
console.log('[FB Tracker] Found button bar via share anchor');
return container;
}
container = container.parentElement;
}
}
// Gather all accessible buttons inside the article
const buttons = Array.from(postElement.querySelectorAll('[role="button"], button'));
const interactionButtons = buttons.filter((button) => {
const info = buttonClassification(button);
return info.isLike || info.isComment || info.isShare;
});
const anchorElements = [
postElement.querySelector('[data-ad-rendering-role="share_button"]'),
postElement.querySelector('[data-ad-rendering-role="comment_button"]'),
postElement.querySelector('[data-ad-rendering-role*="gefällt"]')
].filter(Boolean);
const candidates = new Set(interactionButtons);
anchorElements.forEach((el) => {
const buttonContainer = el.closest('[role="button"]') || el.closest('button');
if (buttonContainer) {
candidates.add(buttonContainer);
}
});
const seen = new Set();
for (const button of candidates) {
const info = buttonClassification(button);
console.log('[FB Tracker] Candidate button', {
label: normalizeButtonLabel(button),
hasLike: info.isLike,
hasComment: info.isComment,
hasShare: info.isShare,
hasReply: info.isReply,
classes: button.className
});
let current = button;
while (current && current !== postElement && current !== document.body) {
if (seen.has(current)) {
current = current.parentElement;
continue;
}
seen.add(current);
const hasBar = hasInteractionButtons(current);
if (hasBar) {
console.log('[FB Tracker] Found button bar');
return current;
}
current = current.parentElement;
}
}
let sibling = postElement.nextElementSibling;
for (let i = 0; i < 6 && sibling; i++) {
if (!seen.has(sibling) && hasInteractionButtons(sibling)) {
console.log('[FB Tracker] Found button bar via next sibling');
return sibling;
}
sibling = sibling.nextElementSibling;
}
sibling = postElement.previousElementSibling;
for (let i = 0; i < 3 && sibling; i++) {
if (!seen.has(sibling) && hasInteractionButtons(sibling)) {
console.log('[FB Tracker] Found button bar via previous sibling');
return sibling;
}
sibling = sibling.previousElementSibling;
}
let parent = postElement.parentElement;
for (let depth = 0; depth < 4 && parent; depth++) {
if (!seen.has(parent) && hasInteractionButtons(parent)) {
console.log('[FB Tracker] Found button bar via parent');
return parent;
}
let next = parent.nextElementSibling;
for (let i = 0; i < 4 && next; i++) {
if (!seen.has(next) && hasInteractionButtons(next)) {
console.log('[FB Tracker] Found button bar via parent sibling');
return next;
}
next = next.nextElementSibling;
}
let prev = parent.previousElementSibling;
for (let i = 0; i < 3 && prev; i++) {
if (!seen.has(prev) && hasInteractionButtons(prev)) {
console.log('[FB Tracker] Found button bar via parent previous sibling');
return prev;
}
prev = prev.previousElementSibling;
}
parent = parent.parentElement;
}
console.log('[FB Tracker] Button bar not found');
return null;
}
function findPostContainers() {
const containers = [];
const seen = new Set();
const candidateSelectors = [
'div[role="dialog"] article',
'div[role="dialog"] div[aria-posinset]',
'[data-pagelet*="FeedUnit"] article',
'div[role="main"] article',
'[data-visualcompletion="ignore-dynamic"] article',
'div[aria-posinset]',
'article[role="article"]',
'article',
'div[data-pagelet*="Reel"]',
'div[data-pagelet*="WatchFeed"]',
'div[data-pagelet*="QPE_PublisherStory"]'
];
if (isOnReelsPage()) {
candidateSelectors.push('div[role="complementary"]');
}
const candidateElements = document.querySelectorAll(candidateSelectors.join(', '));
candidateElements.forEach((element) => {
const container = ensurePrimaryPostElement(element);
if (!container) {
return;
}
if (seen.has(container)) {
return;
}
const buttonBar = findButtonBar(container);
if (!isMainPost(container, buttonBar)) {
return;
}
const likeButton = findLikeButtonWithin(container);
seen.add(container);
if (likeButton || isOnReelsPage()) {
containers.push({ container, likeButton, buttonBar: buttonBar || null });
}
});
return containers;
}
function findLikeButtonWithin(container) {
if (!container) {
return null;
}
const selectors = [
'[data-ad-rendering-role="gefällt"]',
'[data-ad-rendering-role="gefällt mir_button"]',
'[data-ad-rendering-role*="gefällt" i]',
'[aria-label*="gefällt" i]',
'[aria-label*="like" i]',
'div[role="button"][aria-pressed]'
];
for (const selector of selectors) {
const button = container.querySelector(selector);
if (button) {
return button;
}
}
return container.querySelector('div[role="button"]');
}
function findLikeButtonWithin(container) {
if (!container) {
return null;
}
const selectors = [
'[data-ad-rendering-role="gefällt"]',
'[data-ad-rendering-role="gefällt mir_button"]',
'[data-ad-rendering-role*="gefällt" i]',
'[aria-label*="gefällt" i]',
'[aria-label*="like" i]',
'[aria-pressed="true"]',
'div[role="button"]'
];
for (const selector of selectors) {
const button = container.querySelector(selector);
if (button) {
return button;
}
}
return null;
}
function captureScreenshot(screenshotRect) {
return new Promise((resolve) => {
chrome.runtime.sendMessage({ type: 'captureScreenshot', screenshotRect }, (response) => {
if (chrome.runtime.lastError) {
console.warn('[FB Tracker] Screenshot capture failed:', chrome.runtime.lastError.message);
resolve(null);
return;
}
if (!response || response.error) {
if (response && response.error) {
console.warn('[FB Tracker] Screenshot capture reported error:', response.error);
}
resolve(null);
return;
}
if (!response.imageData) {
console.warn('[FB Tracker] Screenshot capture returned no data');
resolve(null);
return;
}
(async () => {
try {
let result = response.imageData;
if (screenshotRect) {
const cropped = await cropScreenshot(result, screenshotRect);
if (cropped) {
result = cropped;
}
}
resolve(result);
} catch (error) {
console.warn('[FB Tracker] Screenshot processing failed:', error);
resolve(response.imageData);
}
})();
});
});
}
async function uploadScreenshot(postId, imageData) {
try {
const response = await backendFetch(`${API_URL}/posts/${postId}/screenshot`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ imageData })
});
if (!response.ok) {
console.warn('[FB Tracker] Screenshot upload failed:', response.status);
}
} catch (error) {
console.error('[FB Tracker] Screenshot upload error:', error);
}
}
async function captureAndUploadScreenshot(postId, postElement) {
const imageData = await captureElementScreenshot(postElement);
if (!imageData) {
return;
}
const optimized = await maybeDownscaleScreenshot(imageData);
await uploadScreenshot(postId, optimized);
}
async function captureElementScreenshot(element) {
if (!element) {
return await captureScreenshot();
}
const horizontalMargin = 32;
const verticalMargin = 96;
const maxSegments = 12;
const delayBetweenScrolls = 200;
const originalScrollX = window.scrollX;
const originalScrollY = window.scrollY;
const devicePixelRatio = window.devicePixelRatio || 1;
const stickyOffset = getStickyHeaderHeight();
const segments = [];
const elementRect = element.getBoundingClientRect();
const elementTop = elementRect.top + window.scrollY;
const elementBottom = elementRect.bottom + window.scrollY;
const documentHeight = document.documentElement.scrollHeight;
const startY = Math.max(0, elementTop - verticalMargin - stickyOffset);
const endY = Math.min(documentHeight, elementBottom + verticalMargin);
const baseDocTop = Math.max(0, elementTop - verticalMargin);
try {
let iteration = 0;
let targetScroll = startY;
while (iteration < maxSegments) {
iteration += 1;
window.scrollTo({ top: targetScroll, left: window.scrollX, behavior: 'auto' });
await delay(delayBetweenScrolls);
const rect = element.getBoundingClientRect();
const viewportHeight = window.innerHeight;
const captureTop = Math.max(0, rect.top - verticalMargin - stickyOffset);
const captureBottom = Math.min(viewportHeight, rect.bottom + verticalMargin);
const captureHeight = captureBottom - captureTop;
if (captureHeight <= 0) {
break;
}
const captureRect = {
left: rect.left - horizontalMargin,
top: captureTop,
width: rect.width + horizontalMargin * 2,
height: captureHeight,
devicePixelRatio
};
const segmentData = await captureScreenshot(captureRect);
if (!segmentData) {
break;
}
const docTop = Math.max(0, window.scrollY + captureTop);
const docBottom = docTop + captureHeight;
segments.push({ data: segmentData, docTop, docBottom });
const reachedBottom = docBottom >= endY - 4;
if (reachedBottom) {
break;
}
const nextScroll = docBottom - Math.max(0, (viewportHeight - stickyOffset) * 0.5);
const maxScroll = Math.max(0, endY - viewportHeight);
targetScroll = Math.min(nextScroll, maxScroll);
if (targetScroll <= window.scrollY + 1) {
targetScroll = window.scrollY + Math.max(160, viewportHeight * 0.6);
}
if (targetScroll <= window.scrollY + 1 || targetScroll >= endY) {
break;
}
}
} finally {
window.scrollTo({ top: originalScrollY, left: originalScrollX, behavior: 'auto' });
}
if (!segments.length) {
return await captureScreenshot();
}
const stitched = await stitchScreenshotSegments(segments, devicePixelRatio, baseDocTop);
return stitched;
}
async function stitchScreenshotSegments(segments, devicePixelRatio, baseDocTop) {
const images = [];
let maxDocBottom = baseDocTop;
for (const segment of segments) {
const img = await loadImage(segment.data);
if (!img) {
continue;
}
images.push({ img, docTop: segment.docTop, docBottom: segment.docBottom });
if (segment.docBottom > maxDocBottom) {
maxDocBottom = segment.docBottom;
}
}
if (!images.length) {
return null;
}
const width = images.reduce((max, item) => Math.max(max, item.img.width), 0);
const totalHeightPx = Math.max(1, Math.round((maxDocBottom - baseDocTop) * devicePixelRatio));
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = totalHeightPx;
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, width, totalHeightPx);
for (const { img, docTop } of images) {
const offsetY = Math.round(Math.max(0, docTop - baseDocTop) * devicePixelRatio);
ctx.drawImage(img, 0, offsetY);
}
return canvas.toDataURL('image/jpeg', 0.85);
}
async function cropScreenshot(imageData, rect) {
if (!rect) {
return imageData;
}
try {
const image = await loadImage(imageData);
if (!image) {
return imageData;
}
const ratio = rect.devicePixelRatio || window.devicePixelRatio || 1;
const rawLeft = (rect.left || 0) * ratio;
const rawTop = (rect.top || 0) * ratio;
const rawWidth = (rect.width || image.width) * ratio;
const rawHeight = (rect.height || image.height) * ratio;
const rawRight = rawLeft + rawWidth;
const rawBottom = rawTop + rawHeight;
const left = Math.max(0, Math.floor(rawLeft));
const top = Math.max(0, Math.floor(rawTop));
const right = Math.min(image.width, Math.ceil(rawRight));
const bottom = Math.min(image.height, Math.ceil(rawBottom));
const width = Math.max(1, right - left);
const height = Math.max(1, bottom - top);
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
ctx.drawImage(image, left, top, width, height, 0, 0, width, height);
return canvas.toDataURL('image/jpeg', 0.85);
} catch (error) {
console.warn('[FB Tracker] Failed to crop screenshot:', error);
return imageData;
}
}
async function maybeDownscaleScreenshot(imageData) {
try {
const maxWidth = 1600;
const current = await loadImage(imageData);
if (!current) {
return imageData;
}
if (current.width <= maxWidth) {
return imageData;
}
const scale = maxWidth / current.width;
const canvas = document.createElement('canvas');
canvas.width = Math.round(current.width * scale);
canvas.height = Math.round(current.height * scale);
const ctx = canvas.getContext('2d');
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = 'high';
ctx.drawImage(current, 0, 0, canvas.width, canvas.height);
return canvas.toDataURL('image/jpeg', 0.8);
} catch (error) {
console.warn('[FB Tracker] Failed to downscale screenshot:', error);
return imageData;
}
}
function getStickyHeaderHeight() {
try {
const banner = document.querySelector('[role="banner"], header[role="banner"]');
if (!banner) {
return 0;
}
const rect = banner.getBoundingClientRect();
if (!rect || !rect.height) {
return 0;
}
return Math.min(rect.height, 160);
} catch (error) {
console.warn('[FB Tracker] Failed to determine sticky header height:', error);
return 0;
}
}
function delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function loadImage(dataUrl) {
return new Promise((resolve) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = () => resolve(null);
img.src = dataUrl;
});
}
function toDateTimeLocalString(date) {
const local = new Date(date.getTime() - date.getTimezoneOffset() * 60000);
return local.toISOString().slice(0, 16);
}
function getNextDayDefaultDeadlineValue() {
const tomorrow = new Date();
tomorrow.setHours(0, 0, 0, 0);
tomorrow.setDate(tomorrow.getDate() + 1);
return toDateTimeLocalString(tomorrow);
}
function extractDeadlineFromPostText(postElement) {
if (!postElement) {
return null;
}
// Get all text content from the post
const textNodes = [];
const walker = document.createTreeWalker(
postElement,
NodeFilter.SHOW_TEXT,
null,
false
);
let node;
while (node = walker.nextNode()) {
if (node.textContent.trim()) {
textNodes.push(node.textContent.trim());
}
}
const fullText = textNodes.join(' ');
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,
'april': 4, 'apr': 4,
'mai': 5,
'juni': 6, 'jun': 6,
'juli': 7, 'jul': 7,
'august': 8, 'aug': 8,
'september': 9, 'sep': 9, 'sept': 9,
'oktober': 10, 'okt': 10,
'november': 11, 'nov': 11,
'dezember': 12, 'dez': 12
};
// German date patterns
const patterns = [
// DD.MM.YYYY or DD.MM.YY (with optional time like ", 23:59Uhr")
/\b(\d{1,2})\.(\d{1,2})\.(\d{2,4})(?!\d)/g,
// DD.MM (without year, optional trailing time)
/\b(\d{1,2})\.(\d{1,2})\.(?!\d)/g
];
const extractTimeAfterIndex = (text, index) => {
const tail = text.slice(index, index + 80);
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);
if (!timeMatch) {
return null;
}
const hour = parseInt(timeMatch[1], 10);
const minute = typeof timeMatch[2] === 'string' && timeMatch[2].length
? parseInt(timeMatch[2], 10)
: 0;
if (Number.isNaN(hour) || Number.isNaN(minute)) {
return null;
}
if (hour < 0 || hour > 23 || minute < 0 || minute > 59) {
return null;
}
return { hour, minute };
};
const hasInclusiveKeywordNear = (text, index) => {
const windowStart = Math.max(0, index - 40);
const windowText = text.slice(windowStart, index).toLowerCase();
return /\b(einschlie(?:ß|ss)lich|inklusive|inkl\.)\b/.test(windowText);
};
const foundDates = [];
for (const pattern of patterns) {
let match;
while ((match = pattern.exec(fullText)) !== null) {
const day = parseInt(match[1], 10);
const month = parseInt(match[2], 10);
let year = match[3] ? parseInt(match[3], 10) : today.getFullYear();
const matchIndex = match.index;
// Handle 2-digit years
if (year < 100) {
year += 2000;
}
// Validate date
if (month >= 1 && month <= 12 && day >= 1 && day <= 31) {
const date = new Date(year, month - 1, day, 0, 0, 0, 0);
// Check if date is valid (e.g., not 31.02.)
if (date.getMonth() === month - 1 && date.getDate() === day) {
const timeInfo = extractTimeAfterIndex(fullText, pattern.lastIndex);
const hasTime = Boolean(timeInfo);
if (timeInfo) {
date.setHours(timeInfo.hour, timeInfo.minute, 0, 0);
} else if (hasInclusiveKeywordNear(fullText, matchIndex)) {
date.setHours(23, 59, 0, 0);
}
const hasInclusiveTime = hasInclusiveKeywordNear(fullText, matchIndex);
const recordHasTime = hasTime || hasInclusiveTime;
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 });
}
}
}
}
}
// 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;
let monthMatch;
while ((monthMatch = monthPattern.exec(fullText)) !== null) {
const day = parseInt(monthMatch[1], 10);
const monthStr = monthMatch[2].toLowerCase();
const month = monthNames[monthStr];
let year = today.getFullYear();
if (monthMatch[3]) {
year = parseInt(monthMatch[3], 10);
if (year < 100) {
year += 2000;
}
}
const matchIndex = monthMatch.index;
if (month && day >= 1 && day <= 31) {
const date = new Date(year, month - 1, day, 0, 0, 0, 0);
// Check if date is valid
if (date.getMonth() === month - 1 && date.getDate() === day) {
const timeInfo = extractTimeAfterIndex(fullText, monthPattern.lastIndex);
const hasTime = Boolean(timeInfo);
if (timeInfo) {
date.setHours(timeInfo.hour, timeInfo.minute, 0, 0);
} else if (hasInclusiveKeywordNear(fullText, matchIndex)) {
date.setHours(23, 59, 0, 0);
}
const hasInclusiveTime = hasInclusiveKeywordNear(fullText, matchIndex);
const recordHasTime = hasTime || hasInclusiveTime;
if (hasInclusiveTime && !hasTime) {
date.setHours(23, 59, 0, 0);
}
// If date has passed this year, assume next year
if (date <= today) {
date.setFullYear(year + 1);
}
foundDates.push({ date, hasTime: recordHasTime });
}
}
}
// Return the earliest future 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);
}
return null;
}
function escapeRegex(value) {
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
function collectKeywordMatches(keywords, text, limit = 20) {
if (!Array.isArray(keywords) || !keywords.length || !text) {
return [];
}
const found = [];
for (const keyword of keywords) {
if (!keyword) continue;
const pattern = new RegExp(`\\b${escapeRegex(keyword)}\\b`, 'gi');
const matches = text.match(pattern);
if (matches && matches.length) {
found.push(keyword);
if (found.length >= limit) {
break;
}
}
}
return Array.from(new Set(found));
}
function collectRegexMatches(regex, text, limit = 20) {
if (!regex || !(regex instanceof RegExp) || !text) {
return [];
}
const matches = Array.from(text.matchAll(regex)).map((m) => m[0]);
if (!matches.length) {
return [];
}
return Array.from(new Set(matches)).slice(0, limit);
}
function filterScorelines(candidates = []) {
const filtered = [];
for (const raw of candidates) {
const parts = raw.split(':').map((part) => part.trim());
if (parts.length !== 2) {
continue;
}
const [a, b] = parts.map((p) => parseInt(p, 10));
if (Number.isNaN(a) || Number.isNaN(b)) {
continue;
}
if (a < 0 || b < 0) {
continue;
}
if (a > 15 || b > 15) {
continue;
}
filtered.push(`${a}:${b}`);
}
return filtered;
}
function evaluateSportsScore(text, moderationSettings = null) {
if (!text || typeof text !== 'string') {
return null;
}
const normalizedText = text.toLowerCase();
const weights = {
...SPORTS_SCORING_DEFAULTS.weights,
...(moderationSettings && moderationSettings.sports_score_weights ? moderationSettings.sports_score_weights : {})
};
const threshold = moderationSettings && typeof moderationSettings.sports_score_threshold === 'number'
? moderationSettings.sports_score_threshold
: SPORTS_SCORING_DEFAULTS.threshold;
const terms = (() => {
const base = DEFAULT_SPORT_TERMS;
const incoming = moderationSettings && moderationSettings.sports_terms ? moderationSettings.sports_terms : null;
const normalizeList = (list, fallback) => {
if (!Array.isArray(list)) return fallback;
const cleaned = list
.map((entry) => typeof entry === 'string' ? entry.trim().toLowerCase() : '')
.filter((entry) => entry);
const unique = Array.from(new Set(cleaned)).slice(0, 200);
return unique.length ? unique : fallback;
};
const src = incoming && typeof incoming === 'object' ? incoming : {};
return {
nouns: normalizeList(src.nouns, base.nouns),
verbs: normalizeList(src.verbs, base.verbs),
competitions: normalizeList(src.competitions, base.competitions),
celebrations: normalizeList(src.celebrations, base.celebrations),
locations: normalizeList(src.locations, base.locations),
negatives: normalizeList(src.negatives, base.negatives)
};
})();
const matchesCount = (regex) => {
if (!regex || !(regex instanceof RegExp)) {
return 0;
}
const matches = normalizedText.match(regex);
return matches ? matches.length : 0;
};
const applyWeight = (count, weight, label, matches = []) => {
if (!count || !weight) {
return 0;
}
const effective = Math.min(count, 5);
const gain = effective * weight;
if (matches && matches.length) {
hitDetails.push(`${label}: ${matches.slice(0, 10).join(', ')}`);
} else {
hitDetails.push(`${label} x${effective} (+${gain.toFixed(1)})`);
}
score += gain;
return gain;
};
const hitDetails = [];
let score = 0;
const scorelineMatchesRaw = collectRegexMatches(/\b\d{1,2}\s*:\s*\d{1,2}\b/g, normalizedText);
const scorelineMatches = filterScorelines(scorelineMatchesRaw);
applyWeight(scorelineMatches.length, weights.scoreline, 'Ergebnis', scorelineMatches);
const scoreEmojiMatches = collectRegexMatches(/[0-9]️⃣/g, normalizedText)
.concat(collectRegexMatches(/\+\s*\d\b/g, normalizedText));
applyWeight(scoreEmojiMatches.length, weights.scoreEmoji, 'Punkte', scoreEmojiMatches);
const sportEmojiMatches = collectRegexMatches(/[⚽🏐🏀🏈🎾🏉🥅🏒🏑🏓🏸🤾🏏🎽🎳🥊🥋⛳]/g, normalizedText);
applyWeight(sportEmojiMatches.length, weights.sportEmoji, 'Sport-Emoji', sportEmojiMatches);
const verbMatches = collectKeywordMatches(terms.verbs, normalizedText);
applyWeight(verbMatches.length, weights.sportVerb, 'Sport-Verben', verbMatches);
const nounMatches = collectKeywordMatches(terms.nouns, normalizedText);
applyWeight(nounMatches.length, weights.sportNoun, 'Sport-Vokabeln', nounMatches);
const hashtagMatches = collectRegexMatches(/#(?:auswärtssieg|heimsieg|derbysieg|bundesliga|liga|pokal|cup|fc[a-z0-9]+|sv[a-z0-9]+|tsv[a-z0-9]+|sg[a-z0-9]+)/g, normalizedText);
applyWeight(hashtagMatches.length, weights.hashtag, 'Sport-Hashtags', hashtagMatches);
const teamMatches = collectRegexMatches(/\b(?:fc|sv|tsv|ssv|bvb|sge|fcb|hsv|vfb|fsv|sg|scl|djk)[\s\-]?[a-zäöüß0-9]+/gi, normalizedText);
applyWeight(teamMatches.length, weights.teamToken, 'Team-Kürzel', teamMatches);
const competitionMatches = collectKeywordMatches(terms.competitions, normalizedText);
applyWeight(competitionMatches.length, weights.competition, 'Liga/Turnier', competitionMatches);
const celebrationMatches = collectKeywordMatches(terms.celebrations, normalizedText);
applyWeight(celebrationMatches.length, weights.celebration, 'Ergebnisbezug', celebrationMatches);
const locationMatches = collectKeywordMatches(terms.locations, normalizedText);
applyWeight(locationMatches.length, weights.location, 'Spielort', locationMatches);
const nonSportMatches = collectKeywordMatches(terms.negatives, normalizedText);
const nonSportHits = nonSportMatches.length;
if (nonSportHits) {
const penalty = Math.min(2, nonSportHits) * 1;
score -= penalty;
hitDetails.push(`Gegenindizien: ${nonSportMatches.slice(0, 10).join(', ')}`);
}
const finalScore = Math.round(score * 10) / 10;
return {
score: finalScore,
threshold,
wouldHide: finalScore >= threshold,
hits: hitDetails
};
}
function buildSportsScoreBadge(scoreInfo) {
if (!scoreInfo) {
return null;
}
// Show badge only for strictly positive scores; hide zero/negative
if (typeof scoreInfo.score !== 'number' || scoreInfo.score <= 0) {
return null;
}
const badge = document.createElement('span');
const wouldHide = !!scoreInfo.wouldHide;
const bg = wouldHide ? 'rgba(245, 158, 11, 0.18)' : 'rgba(59, 130, 246, 0.12)';
const border = wouldHide ? 'rgba(245, 158, 11, 0.5)' : 'rgba(59, 130, 246, 0.35)';
const color = wouldHide ? '#b45309' : '#1d4ed8';
badge.className = 'fb-tracker-score-badge';
badge.textContent = `Sport-Score ${scoreInfo.score.toFixed(1)} / ${scoreInfo.threshold}`;
if (scoreInfo.hits && scoreInfo.hits.length) {
const lines = scoreInfo.hits.map((hit) => `• ${hit}`).join('\n');
badge.title = `${lines}\n${wouldHide ? '≥ Schwellwert' : '< Schwellwert'}`;
} else {
badge.title = wouldHide ? 'Über Schwellwert' : 'Unter Schwellwert';
}
badge.style.cssText = `
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 10px;
background: ${bg};
border: 1px solid ${border};
border-radius: 999px;
font-weight: 600;
color: ${color};
font-size: 12px;
`;
return badge;
}
function normalizeFacebookPostUrl(rawValue) {
if (typeof rawValue !== 'string') {
return '';
}
let value = rawValue.trim();
if (!value) {
return '';
}
const trackingIndex = value.indexOf('__cft__');
if (trackingIndex !== -1) {
value = value.slice(0, trackingIndex);
}
value = value.replace(/[?&]$/, '');
let parsed;
try {
parsed = new URL(value);
} catch (error) {
try {
parsed = new URL(value, 'https://www.facebook.com');
} catch (fallbackError) {
return '';
}
}
if (!parsed.hostname.toLowerCase().endsWith('facebook.com')) {
return '';
}
const normalizedPathBeforeTrim = parsed.pathname.replace(/\/+$/, '') || '/';
const lowerPathBeforeTrim = normalizedPathBeforeTrim.toLowerCase();
const watchId = parsed.searchParams.get('v') || parsed.searchParams.get('video_id');
if ((lowerPathBeforeTrim === '/watch' || lowerPathBeforeTrim === '/video.php') && watchId) {
parsed.pathname = `/reel/${watchId}/`;
parsed.search = '';
} else {
const reelMatch = lowerPathBeforeTrim.match(/^\/reel\/([^/]+)$/);
if (reelMatch) {
parsed.pathname = `/reel/${reelMatch[1]}/`;
parsed.search = '';
}
}
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__')
|| lowerKey.startsWith('__eep__')
|| lowerKey.startsWith('mibextid')
|| lowerKey === 'set'
|| lowerKey === 'comment_id'
|| lowerKey === 'hoisted_section_header_type'
|| isSingleUnitParam
) {
return;
}
cleanedParams.append(paramKey, paramValue);
});
const multiPermalinkId = cleanedParams.get('multi_permalinks');
if (multiPermalinkId) {
cleanedParams.delete('multi_permalinks');
const groupMatch = parsed.pathname.match(/^\/groups\/([A-Za-z0-9\.\-_]+)/);
if (groupMatch && multiPermalinkId.match(/^[0-9]+$/)) {
parsed.pathname = `/groups/${groupMatch[1]}/posts/${multiPermalinkId}`;
} else if (groupMatch) {
parsed.pathname = `/groups/${groupMatch[1]}/permalink/${multiPermalinkId}`;
}
}
const normalizedPath = parsed.pathname.replace(/\/+$/, '').toLowerCase();
if (normalizedPath.startsWith('/hashtag/') || normalizedPath.startsWith('/watch/hashtag/')) {
return '';
}
const search = cleanedParams.toString();
const formatted = `${parsed.origin}${parsed.pathname}${search ? `?${search}` : ''}`;
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 = `
Tracker: ${statusText}${completed ? ' ✓' : ''}${expiredText}
${lastCheck ? `Letzte: ${lastCheck}
` : ''}
`;
if (canCurrentProfileCheck && !isExpired && !completed) {
statusHtml += `
`;
} else if (isCurrentProfileDone) {
statusHtml += `
✓ Von dir bestätigt
`;
}
container.innerHTML = statusHtml;
if (postData.id) {
const actionsContainer = ensureTrackerActionsContainer(container);
if (actionsContainer) {
const webAppUrl = (() => {
try {
const baseUrl = `${WEBAPP_BASE_URL}/`;
const url = new URL('', baseUrl);
url.searchParams.set('tab', 'all');
url.searchParams.set('postId', String(postData.id));
if (postData.url) {
url.searchParams.set('postUrl', postData.url);
}
return url.toString();
} catch (error) {
console.debug('[FB Tracker] Failed to build WebApp URL, falling back to base:', error);
return `${WEBAPP_BASE_URL}/?tab=all`;
}
})();
let webAppLink = actionsContainer.querySelector('.fb-tracker-webapp-link');
if (!webAppLink) {
webAppLink = document.createElement('a');
webAppLink.className = 'fb-tracker-webapp-link';
webAppLink.target = '_blank';
webAppLink.rel = 'noopener noreferrer';
webAppLink.setAttribute('aria-label', 'In der Webapp anzeigen');
webAppLink.title = 'In der Webapp anzeigen';
webAppLink.textContent = '📋';
webAppLink.style.cssText = `
text-decoration: none;
font-size: 18px;
line-height: 1;
padding: 4px 6px;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 6px;
color: inherit;
transition: background-color 0.2s ease, transform 0.2s ease;
cursor: pointer;
`;
webAppLink.addEventListener('mouseenter', () => {
webAppLink.style.backgroundColor = 'rgba(0, 0, 0, 0.08)';
webAppLink.style.transform = 'translateY(-1px)';
});
webAppLink.addEventListener('mouseleave', () => {
webAppLink.style.backgroundColor = 'transparent';
webAppLink.style.transform = 'translateY(0)';
});
actionsContainer.insertBefore(webAppLink, actionsContainer.firstChild);
}
webAppLink.href = webAppUrl;
}
}
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
postElement = ensurePrimaryPostElement(postElement);
let existingUI = getTrackerElementForPost(postElement);
if (!existingUI) {
existingUI = postElement.querySelector('.fb-tracker-ui');
if (existingUI && existingUI.isConnected) {
setTrackerElementForPost(postElement, existingUI);
}
}
if (existingUI && !existingUI.isConnected) {
clearTrackerElementForPost(postElement, existingUI);
existingUI = null;
}
if (existingUI) {
console.log('[FB Tracker] Post #' + postNum + ' - UI already exists on normalized element');
postElement.setAttribute(PROCESSED_ATTR, '1');
return;
}
// Mark immediately to prevent duplicate creation during async operations
if (postElement.getAttribute(PROCESSED_ATTR) === '1') {
console.log('[FB Tracker] Post #' + postNum + ' - Already processed:', postElement);
return;
}
postElement.setAttribute(PROCESSED_ATTR, '1');
const postUrlData = getPostUrl(postElement, postNum);
if (!postUrlData.url) {
console.log('[FB Tracker] Post #' + postNum + ' - No URL found:', postElement);
postElement.removeAttribute(PROCESSED_ATTR);
clearTrackerElementForPost(postElement);
return;
}
console.log('[FB Tracker] Post #' + postNum + ' - Creating tracker UI for:', postUrlData.url, postElement);
const encodedUrl = encodeURIComponent(postUrlData.url);
const existingEntry = processedPostUrls.get(encodedUrl);
if (existingEntry && existingEntry.element && existingEntry.element !== postElement) {
if (document.body.contains(existingEntry.element)) {
existingEntry.element.removeAttribute(PROCESSED_ATTR);
clearTrackerElementForPost(existingEntry.element);
} else {
processedPostUrls.delete(encodedUrl);
}
const otherUI = document.querySelector(`.fb-tracker-ui[data-post-url="${encodedUrl}"]`);
if (otherUI) {
otherUI.remove();
}
}
const { likeButton: sourceLikeButton = null, isSearchResult = false, isDialogContext = false } = options;
const currentPath = window.location.pathname || '/';
const isFeedHome = FEED_HOME_PATHS.includes(currentPath);
const likedByCurrentUser = isPostLikedByCurrentUser(sourceLikeButton, postElement);
// Create UI container
const container = document.createElement('div');
container.className = 'fb-tracker-ui';
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;
border-top: 1px solid #e4e6eb;
display: flex;
flex-wrap: wrap;
flex-direction: row;
align-items: center;
gap: 8px;
row-gap: 6px;
width: 100%;
box-sizing: border-box;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
font-size: 13px;
`;
// Check current status (check all URL candidates to avoid duplicates)
const profileNumber = await getProfileNumber();
const postData = await checkPostStatus(postUrlData.url, postUrlData.allCandidates);
const isTracked = !!postData;
let searchTrackingInfo = null;
if (isSearchResult) {
const cacheKey = encodedUrl;
const alreadyRecorded = sessionSearchRecordedUrls.has(cacheKey);
const latestInfo = await recordSearchResultPost(postUrlData.url, postUrlData.allCandidates, {
skipIncrement: alreadyRecorded
});
if (!alreadyRecorded && latestInfo) {
sessionSearchRecordedUrls.add(cacheKey);
}
if (latestInfo) {
sessionSearchInfoCache.set(cacheKey, latestInfo);
searchTrackingInfo = latestInfo;
} else if (sessionSearchInfoCache.has(cacheKey)) {
searchTrackingInfo = sessionSearchInfoCache.get(cacheKey);
} else {
searchTrackingInfo = latestInfo;
}
}
let manualHideInfo = null;
if (!isSearchResult && isFeedHome) {
try {
manualHideInfo = await recordSearchResultPost(postUrlData.url, postUrlData.allCandidates, {
skipIncrement: true
});
} catch (error) {
console.debug('[FB Tracker] Manual hide lookup failed:', error);
}
}
if (isSearchResult && (isTracked || likedByCurrentUser)) {
if (isDialogContext) {
console.log('[FB Tracker] Post #' + postNum + ' - Would hide on search results (tracked or liked) but skipping in dialog context');
} else {
console.log('[FB Tracker] Post #' + postNum + ' - Hidden on search results (tracked or liked)');
hidePostElement(postElement);
processedPostUrls.set(encodedUrl, {
element: postElement,
createdAt: Date.now(),
hidden: true,
searchSeenCount: searchTrackingInfo && typeof searchTrackingInfo.seen_count === 'number'
? searchTrackingInfo.seen_count
: null
});
return;
}
}
if (searchTrackingInfo && searchTrackingInfo.should_hide && !isTracked && !likedByCurrentUser) {
if (isDialogContext) {
console.log('[FB Tracker] Post #' + postNum + ' - Would hide on search results after repeated sightings but skipping in dialog context:', searchTrackingInfo);
} else {
console.log('[FB Tracker] Post #' + postNum + ' - Hidden on search results after repeated sightings:', searchTrackingInfo);
hidePostElement(postElement);
processedPostUrls.set(encodedUrl, {
element: postElement,
createdAt: Date.now(),
hidden: true,
searchSeenCount: typeof searchTrackingInfo.seen_count === 'number' ? searchTrackingInfo.seen_count : null
});
return;
}
}
if (!isSearchResult && manualHideInfo && (manualHideInfo.manually_hidden || manualHideInfo.should_hide)) {
if (isDialogContext) {
console.log('[FB Tracker] Post #' + postNum + ' - Would hide on feed (manually hidden) but skipping in dialog context:', manualHideInfo);
} else {
console.log('[FB Tracker] Post #' + postNum + ' - Hidden on feed (manually hidden):', manualHideInfo);
hidePostElement(postElement);
processedPostUrls.set(encodedUrl, {
element: postElement,
createdAt: Date.now(),
hidden: true,
searchSeenCount: typeof manualHideInfo.seen_count === 'number' ? manualHideInfo.seen_count : null
});
return;
}
}
if (postData) {
const renderResult = await renderTrackedStatus({
container,
postElement,
postData,
profileNumber,
isFeedHome,
isDialogContext,
manualHideInfo,
encodedUrl,
postNum
});
if (renderResult && renderResult.hidden) {
return;
}
} else {
// Post not tracked - show add UI
const selectId = `tracker-select-${Date.now()}`;
const deadlineId = `tracker-deadline-${Date.now()}`;
container.innerHTML = `
`;
// Add click handler for the button
const addButton = container.querySelector('.fb-tracker-add-btn');
const selectElement = container.querySelector(`#${selectId}`);
const deadlineInput = container.querySelector(`#${deadlineId}`);
selectElement.value = '2';
if (deadlineInput) {
// Try to extract deadline from post text first
const extractedDeadline = extractDeadlineFromPostText(postElement);
deadlineInput.value = extractedDeadline || getNextDayDefaultDeadlineValue();
}
addButton.addEventListener('click', async () => {
const targetCount = parseInt(selectElement.value, 10);
console.log('[FB Tracker] Add button clicked, target:', targetCount);
addButton.disabled = true;
addButton.textContent = 'Wird hinzugefügt...';
postElement.scrollIntoView({ behavior: 'auto', block: 'start', inline: 'nearest' });
await delay(220);
const deadlineValue = deadlineInput ? deadlineInput.value : '';
const result = await addPostToTracking(postUrlData.url, targetCount, profileNumber, {
postElement,
deadline: deadlineValue,
candidates: postUrlData.allCandidates
});
if (result) {
const renderOutcome = await renderTrackedStatus({
container,
postElement,
postData: result,
profileNumber,
isFeedHome,
isDialogContext,
manualHideInfo,
encodedUrl,
postNum
});
if (renderOutcome && renderOutcome.hidden) {
return;
}
return;
} else {
// Error
addButton.disabled = false;
addButton.textContent = 'Fehler - Erneut versuchen';
addButton.style.backgroundColor = '#e74c3c';
if (deadlineInput) {
deadlineInput.value = getNextDayDefaultDeadlineValue();
}
}
});
console.log('[FB Tracker] Post #' + postNum + ' - UI created for new post');
// Add AI button for new posts
await addAICommentButton(container, postElement);
}
let sportsScoreInfo = null;
try {
const moderationSettings = await fetchModerationSettings();
if (moderationSettings && moderationSettings.sports_scoring_enabled !== false) {
const postTextForScore = extractPostText(postElement);
if (postTextForScore) {
sportsScoreInfo = evaluateSportsScore(postTextForScore, moderationSettings);
}
if (
moderationSettings.sports_auto_hide_enabled
&& sportsScoreInfo
&& sportsScoreInfo.wouldHide
&& !isTracked
&& !likedByCurrentUser
) {
if (isDialogContext) {
console.log('[FB Tracker] Post #' + postNum + ' - Would auto-hide by sports score but skipping in dialog context');
} else {
console.log('[FB Tracker] Post #' + postNum + ' - Auto-hidden by sports score', sportsScoreInfo);
try {
await recordSearchResultPost(postUrlData.url, postUrlData.allCandidates, { forceHide: true, sportsAutoHide: true });
} catch (error) {
console.debug('[FB Tracker] Auto-hide scoring could not persist hide state:', error);
}
hidePostElement(postElement);
processedPostUrls.set(encodedUrl, {
element: postElement,
createdAt: Date.now(),
hidden: true,
searchSeenCount: searchTrackingInfo && typeof searchTrackingInfo.seen_count === 'number'
? searchTrackingInfo.seen_count
: null
});
return;
}
}
}
} catch (error) {
console.debug('[FB Tracker] Sport-Scoring nicht verfügbar:', error);
}
if (isSearchResult) {
const info = document.createElement('button');
info.type = 'button';
info.className = 'fb-tracker-search-info';
info.title = 'Beitrag künftig in den Suchergebnissen ausblenden';
info.setAttribute('aria-label', 'Beitrag künftig in den Suchergebnissen ausblenden');
const countText = searchTrackingInfo && typeof searchTrackingInfo.seen_count === 'number'
? searchTrackingInfo.seen_count
: (isTracked ? 'gespeichert' : 'n/v');
const iconSpan = document.createElement('span');
iconSpan.setAttribute('aria-hidden', 'true');
iconSpan.style.fontSize = '15px';
const countSpan = document.createElement('span');
countSpan.textContent = countText;
info.appendChild(iconSpan);
info.appendChild(countSpan);
info.style.cssText = `
color: #1d2129;
font-weight: 600;
border-radius: 999px;
padding: 4px 12px;
background: rgba(255, 255, 255, 0.98);
border: 1px solid rgba(29, 33, 41, 0.18);
display: inline-flex;
align-items: center;
gap: 6px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08);
transition: transform 150ms ease, box-shadow 150ms ease, opacity 120ms ease;
cursor: pointer;
outline: none;
`;
const OPEN_EYES_ICON = '😳';
const CLOSED_EYES_ICON = '😌';
const setIconOpen = () => {
iconSpan.textContent = OPEN_EYES_ICON;
};
const setIconClosed = () => {
iconSpan.textContent = CLOSED_EYES_ICON;
};
setIconOpen();
const resetHover = () => {
setIconOpen();
info.style.transform = 'scale(1)';
info.style.boxShadow = '0 1px 2px rgba(0, 0, 0, 0.08)';
};
info.addEventListener('mouseenter', () => {
info.style.transform = 'translateY(-1px) scale(1.05)';
info.style.boxShadow = '0 6px 14px rgba(0, 0, 0, 0.16)';
setIconClosed();
});
info.addEventListener('mouseleave', () => {
resetHover();
});
info.addEventListener('focus', () => {
info.style.transform = 'translateY(-1px) scale(1.05)';
info.style.boxShadow = '0 6px 14px rgba(24, 119, 242, 0.35)';
setIconClosed();
});
info.addEventListener('blur', () => {
resetHover();
});
info.addEventListener('mousedown', () => {
info.style.transform = 'translateY(0) scale(0.96)';
info.style.boxShadow = '0 3px 8px rgba(0, 0, 0, 0.18)';
});
info.addEventListener('mouseup', () => {
info.style.transform = 'translateY(-1px) scale(1.03)';
info.style.boxShadow = '0 6px 14px rgba(0, 0, 0, 0.16)';
});
const handleManualHide = async (event) => {
event.preventDefault();
event.stopPropagation();
if (postElement.getAttribute('data-fb-tracker-hidden') === '1') {
return;
}
info.disabled = true;
info.style.cursor = 'progress';
info.style.opacity = '0.75';
let hideResult = null;
try {
hideResult = await recordSearchResultPost(postUrlData.url, postUrlData.allCandidates, {
forceHide: true
});
} catch (error) {
console.error('[FB Tracker] Failed to hide search result manually:', error);
}
if (!hideResult) {
info.disabled = false;
info.style.cursor = 'pointer';
info.style.opacity = '1';
resetHover();
return;
}
if (isSearchResult) {
const cacheKeyForHide = encodedUrl;
sessionSearchRecordedUrls.add(cacheKeyForHide);
sessionSearchInfoCache.set(cacheKeyForHide, hideResult);
}
setIconClosed();
if (isDialogContext) {
console.log('[FB Tracker] Post #' + postNum + ' - Manual hide skipped in dialog context');
} else {
hidePostElement(postElement);
const seenCountValue = typeof hideResult.seen_count === 'number' ? hideResult.seen_count : null;
processedPostUrls.set(encodedUrl, {
element: postElement,
createdAt: Date.now(),
hidden: true,
searchSeenCount: seenCountValue
});
}
};
info.addEventListener('click', handleManualHide);
resetHover();
container.insertBefore(info, container.firstChild);
const sportsScoreBadge = buildSportsScoreBadge(sportsScoreInfo);
if (sportsScoreBadge) {
container.insertBefore(sportsScoreBadge, info.nextSibling);
}
} else {
const sportsScoreBadge = buildSportsScoreBadge(sportsScoreInfo);
if (sportsScoreBadge) {
container.insertBefore(sportsScoreBadge, container.firstChild);
}
}
// Insert UI - try multiple strategies to find stable insertion point
let inserted = false;
const tryInsertBeforeReelsCommentComposer = () => {
const textboxCandidates = postElement ? postElement.querySelectorAll('div[role="textbox"]') : [];
const composerElement = Array.from(textboxCandidates).find((element) => {
const ariaLabel = (element.getAttribute('aria-label') || '').toLowerCase();
const ariaPlaceholder = (element.getAttribute('aria-placeholder') || '').toLowerCase();
const combined = `${ariaLabel} ${ariaPlaceholder}`;
return combined.includes('komment') || combined.includes('comment');
});
if (!composerElement) {
return false;
}
const anchorRoot = composerElement.closest('form[role="presentation"]')
|| composerElement.closest('form')
|| composerElement.parentElement;
if (!anchorRoot || !anchorRoot.parentElement) {
return false;
}
anchorRoot.parentElement.insertBefore(container, anchorRoot);
console.log('[FB Tracker] Post #' + postNum + ' - UI inserted before comment composer (Reels complementary). ID: #' + container.id);
return true;
};
if (!inserted && isOnReelsPage() && postElement && postElement.matches('div[role="complementary"]')) {
inserted = tryInsertBeforeReelsCommentComposer();
}
// Strategy 1: After button bar's parent (more stable)
if (!inserted && buttonBar && buttonBar.parentElement && buttonBar.parentElement.parentElement) {
const grandParent = buttonBar.parentElement.parentElement;
grandParent.insertBefore(container, buttonBar.parentElement.nextSibling);
console.log('[FB Tracker] Post #' + postNum + ' - UI inserted after button bar parent. ID: #' + container.id);
inserted = true;
}
// Strategy 2: After button bar directly
if (!inserted && buttonBar && buttonBar.parentElement) {
buttonBar.parentElement.insertBefore(container, buttonBar.nextSibling);
console.log('[FB Tracker] Post #' + postNum + ' - UI inserted after button bar. ID: #' + container.id);
inserted = true;
}
// Strategy 3: Append to post element
if (!inserted) {
postElement.appendChild(container);
console.log('[FB Tracker] Post #' + postNum + ' - UI inserted into article (fallback). ID: #' + container.id);
inserted = true;
}
if (inserted) {
processedPostUrls.set(encodedUrl, {
element: postElement,
createdAt: Date.now(),
searchSeenCount: searchTrackingInfo && typeof searchTrackingInfo.seen_count === 'number'
? searchTrackingInfo.seen_count
: null,
hidden: false
});
setTrackerElementForPost(postElement, container);
}
// Monitor if the UI gets removed and re-insert it
if (inserted) {
const observer = new MutationObserver((mutations) => {
if (!document.getElementById(container.id)) {
console.log('[FB Tracker] Post #' + postNum + ' - UI was removed, re-inserting...');
observer.disconnect();
// Try to re-insert
if (buttonBar && buttonBar.parentElement && buttonBar.parentElement.parentElement) {
buttonBar.parentElement.parentElement.insertBefore(container, buttonBar.parentElement.nextSibling);
} else if (postElement.parentElement) {
postElement.parentElement.appendChild(container);
}
if (container.isConnected) {
setTrackerElementForPost(postElement, container);
}
}
});
// Observe the parent for changes
const observeTarget = container.parentElement || postElement;
observer.observe(observeTarget, { childList: true, subtree: false });
// Stop observing after 5 seconds
setTimeout(() => observer.disconnect(), 5000);
}
}
// Check if article is a main post (not a comment)
function isMainPost(article, buttonBar) {
if (!article || article === document.body) {
return false;
}
if (isOnReelsPage()) {
if (article.matches('div[role="complementary"]') || article.closest('div[role="complementary"]')) {
return true;
}
}
const roleDescription = (article.getAttribute('aria-roledescription') || '').toLowerCase();
if (roleDescription && (roleDescription.includes('kommentar') || roleDescription.includes('comment'))) {
return false;
}
if (article.matches('[data-testid*="comment" i], [data-scope="comment"]')) {
return false;
}
if (article.closest('[data-testid*="comment" i]')) {
return false;
}
if (buttonBar) {
const shareIndicator = buttonBar.querySelector('[data-ad-rendering-role="share_button"]');
if (shareIndicator) {
return true;
}
}
// Comments are usually nested inside other articles or comment sections
// Check if this article is inside another article (likely a comment)
let parent = article.parentElement;
while (parent && parent !== document.body) {
if (parent.getAttribute('role') === 'article') {
const parentRoleDescription = (parent.getAttribute('aria-roledescription') || '').toLowerCase();
if (parentRoleDescription.includes('kommentar') || parentRoleDescription.includes('comment')) {
return false;
}
// This article is inside another article - it's a comment
return false;
}
parent = parent.parentElement;
}
// Additional check: Main posts usually have a link with /posts/ or /permalink/
const postLinks = article.querySelectorAll('a[href*="/posts/"], a[href*="/permalink/"], a[href*="/photo"], a[href*="/videos/"], a[href*="/reel/"]');
if (postLinks.length === 0) {
if (article.querySelector('[data-testid*="comment" i]')) {
return false;
}
if (article.querySelector('[data-ad-rendering-role="share_button"]')) {
return true;
}
const nextSibling = article.nextElementSibling;
if (nextSibling && nextSibling.querySelector('[data-ad-rendering-role="share_button"]')) {
const nextButtons = nextSibling.querySelectorAll('[role="button"]');
if (nextButtons.length > 30) {
return true;
}
}
// No post-type links found - likely a comment
return false;
}
return true;
}
// Global post counter for unique IDs
let globalPostCounter = 0;
// Find all Facebook posts on the page
function findPosts() {
console.log('[FB Tracker] Scanning for posts...');
const postContainers = findPostContainers();
const seenFeedContainers = new Set();
const seenDialogContainers = new Set();
console.log('[FB Tracker] Found', postContainers.length, 'candidate containers');
let processed = 0;
const pathname = window.location.pathname || '';
const isSearchResultsPage = typeof pathname === 'string' && pathname.startsWith(SEARCH_RESULTS_PATH_PREFIX);
for (const { container: originalContainer, likeButton, buttonBar: precomputedButtonBar } of postContainers) {
let container = ensurePrimaryPostElement(originalContainer);
const dialogRoot = container.closest(DIALOG_ROOT_SELECTOR);
const isInDialog = !!dialogRoot;
const seenSet = isInDialog ? seenDialogContainers : seenFeedContainers;
if (seenSet.has(container)) {
continue;
}
const isPending = container.getAttribute(PENDING_ATTR) === '1';
if (isPending) {
seenSet.add(container);
continue;
}
let existingTracker = getTrackerElementForPost(container);
if (!existingTracker) {
existingTracker = container.querySelector('.fb-tracker-ui');
if (existingTracker && existingTracker.isConnected) {
setTrackerElementForPost(container, existingTracker);
}
}
if (existingTracker && !existingTracker.isConnected) {
clearTrackerElementForPost(container, existingTracker);
existingTracker = null;
}
const alreadyProcessed = container.getAttribute(PROCESSED_ATTR) === '1';
const trackerDialogRoot = existingTracker ? existingTracker.closest(DIALOG_ROOT_SELECTOR) : null;
const trackerInSameDialog = Boolean(
existingTracker
&& existingTracker.isConnected
&& (
trackerDialogRoot === dialogRoot
|| (dialogRoot && dialogRoot.contains(existingTracker))
)
);
if (isInDialog) {
if (trackerInSameDialog) {
seenSet.add(container);
continue;
}
if (existingTracker && existingTracker.isConnected) {
existingTracker.remove();
clearTrackerElementForPost(container, existingTracker);
}
if (alreadyProcessed) {
container.removeAttribute(PROCESSED_ATTR);
}
} else {
if (alreadyProcessed || (existingTracker && existingTracker.isConnected)) {
seenSet.add(container);
continue;
}
}
const buttonBar = precomputedButtonBar || findButtonBar(container);
if (!buttonBar) {
console.log('[FB Tracker] Proceeding without button bar, will fallback to post container insertion');
}
if (!isMainPost(container, buttonBar)) {
console.log('[FB Tracker] Skipping non-main post container');
continue;
}
seenSet.add(container);
processed++;
globalPostCounter++;
const postNum = globalPostCounter;
console.log('[FB Tracker] Post #' + postNum + ' - Adding tracker:', container);
container.setAttribute(PENDING_ATTR, '1');
createTrackerUI(container, buttonBar, postNum, {
likeButton,
isSearchResult: isSearchResultsPage,
isDialogContext: isInDialog
}).catch((error) => {
console.error('[FB Tracker] Post #' + postNum + ' - Failed to create UI:', error, container);
}).finally(() => {
container.removeAttribute(PENDING_ATTR);
});
}
console.log('[FB Tracker] Total processed posts this scan:', processed, '| Global count:', globalPostCounter);
}
// Initialize
console.log('[FB Tracker] Initializing...');
// Run multiple times to catch loading posts
setTimeout(findPosts, 2000);
setTimeout(findPosts, 4000);
setTimeout(findPosts, 6000);
// Debounced scan function
let scanTimeout = null;
function scheduleScan() {
if (scanTimeout) {
clearTimeout(scanTimeout);
}
scanTimeout = setTimeout(() => {
console.log('[FB Tracker] Scheduled scan triggered');
findPosts();
}, 1000);
}
// Watch for new posts being added to the page
const observer = new MutationObserver((mutations) => {
scheduleScan();
});
// Start observing
observer.observe(document.body, {
childList: true,
subtree: true
});
// Trigger scan on scroll (for infinite scroll)
let scrollTimeout = null;
window.addEventListener('scroll', () => {
if (scrollTimeout) {
clearTimeout(scrollTimeout);
}
scrollTimeout = setTimeout(() => {
console.log('[FB Tracker] Scroll detected, scanning...');
findPosts();
}, 1000);
});
// Use IntersectionObserver to detect when posts become visible
const visibilityObserver = new IntersectionObserver((entries) => {
let needsScan = false;
entries.forEach(entry => {
if (entry.isIntersecting) {
const postContainer = entry.target;
// Check if already processed
if (postContainer.getAttribute(PROCESSED_ATTR) !== '1') {
console.log('[FB Tracker] Post became visible and not yet processed:', postContainer);
needsScan = true;
}
}
});
if (needsScan) {
// Delay scan slightly to let Facebook finish loading the post content
setTimeout(() => {
console.log('[FB Tracker] Scanning newly visible posts...');
findPosts();
}, 500);
}
}, {
root: null,
rootMargin: '50px',
threshold: 0.1
});
// Watch for new posts and observe them
const postObserver = new MutationObserver((mutations) => {
// Find all posts and observe them for visibility
const postSelector = isOnReelsPage()
? 'div[aria-posinset], div[role="complementary"]'
: 'div[aria-posinset]';
const posts = document.querySelectorAll(postSelector);
posts.forEach(post => {
if (!post.dataset.trackerObserved) {
post.dataset.trackerObserved = 'true';
visibilityObserver.observe(post);
}
});
scheduleScan();
});
// Start observing
postObserver.observe(document.body, {
childList: true,
subtree: true
});
// Initial observation of existing posts
const initialPostSelector = isOnReelsPage()
? 'div[aria-posinset], div[role="complementary"]'
: 'div[aria-posinset]';
const initialPosts = document.querySelectorAll(initialPostSelector);
initialPosts.forEach(post => {
post.dataset.trackerObserved = 'true';
visibilityObserver.observe(post);
});
console.log('[FB Tracker] Observer with IntersectionObserver started');
// Store the element where context menu was opened
let contextMenuTarget = null;
document.addEventListener('contextmenu', (event) => {
contextMenuTarget = event.target;
console.log('[FB Tracker] Context menu opened on:', contextMenuTarget);
}, true);
// Listen for manual reparse command
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message && message.type === 'generateSelectionAI') {
handleSelectionAIRequest(message.selectionText || '', sendResponse);
return true;
}
if (message && message.type === 'reparsePost') {
console.log('[FB Tracker] Manual reparse triggered');
// Use the stored context menu target, fallback to elementFromPoint
let clickedElement = contextMenuTarget;
if (!clickedElement && message.x !== undefined && message.y !== undefined) {
clickedElement = document.elementFromPoint(message.x, message.y);
}
if (!clickedElement) {
console.log('[FB Tracker] No element found');
sendResponse({ success: false });
return true;
}
console.log('[FB Tracker] Searching for post container starting from:', clickedElement);
// Find the post container (aria-posinset)
let postContainer = clickedElement.closest('div[aria-posinset]');
if (!postContainer && isOnReelsPage()) {
postContainer = clickedElement.closest('div[role="complementary"]');
}
if (!postContainer) {
console.log('[FB Tracker] No post container found for clicked element:', clickedElement);
sendResponse({ success: false });
return true;
}
console.log('[FB Tracker] Found post container:', postContainer);
const normalizedContainer = ensurePrimaryPostElement(postContainer);
if (normalizedContainer && normalizedContainer !== postContainer) {
console.log('[FB Tracker] Normalized post container to:', normalizedContainer);
postContainer = normalizedContainer;
}
// Remove processed attribute and existing UI
postContainer.removeAttribute(PROCESSED_ATTR);
const existingUI = postContainer.querySelector('.fb-tracker-ui');
if (existingUI) {
existingUI.remove();
clearTrackerElementForPost(postContainer, existingUI);
console.log('[FB Tracker] Removed existing UI');
}
// Find button bar and create UI
let buttonBar = findButtonBar(postContainer);
if (!buttonBar) {
let fallback = postContainer.parentElement;
while (!buttonBar && fallback && fallback !== document.body) {
buttonBar = findButtonBar(fallback);
fallback = fallback.parentElement;
}
}
if (!buttonBar) {
console.log('[FB Tracker] No button bar found for this post, proceeding with fallback');
}
globalPostCounter++;
const postNum = globalPostCounter;
console.log('[FB Tracker] Reparsing post as #' + postNum);
createTrackerUI(postContainer, buttonBar, postNum).then(() => {
sendResponse({ success: true });
}).catch((error) => {
console.error('[FB Tracker] Failed to reparse:', error);
sendResponse({ success: false });
});
return true;
}
});
// ============================================================================
// AI COMMENT GENERATION
// ============================================================================
/**
* Show a toast notification
*/
function showToast(message, type = 'info') {
const toast = document.createElement('div');
toast.style.cssText = `
position: fixed;
bottom: 24px;
right: 24px;
background: ${type === 'error' ? '#e74c3c' : type === 'success' ? '#42b72a' : '#1877f2'};
color: white;
padding: 12px 20px;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
font-size: 14px;
z-index: 999999;
max-width: 350px;
animation: slideIn 0.3s ease-out;
`;
toast.textContent = message;
// Add animation keyframes
if (!document.getElementById('fb-tracker-toast-styles')) {
const style = document.createElement('style');
style.id = 'fb-tracker-toast-styles';
style.textContent = `
@keyframes slideIn {
from {
transform: translateX(400px);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
@keyframes slideOut {
from {
transform: translateX(0);
opacity: 1;
}
to {
transform: translateX(400px);
opacity: 0;
}
}
`;
document.head.appendChild(style);
}
document.body.appendChild(toast);
setTimeout(() => {
toast.style.animation = 'slideOut 0.3s ease-out';
setTimeout(() => toast.remove(), 300);
}, 3000);
}
async function copyTextToClipboard(text) {
if (typeof text !== 'string') {
return false;
}
if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') {
try {
await navigator.clipboard.writeText(text);
return true;
} catch (error) {
console.warn('[FB Tracker] navigator.clipboard.writeText failed:', error);
}
}
try {
const textarea = document.createElement('textarea');
textarea.value = text;
textarea.style.position = 'fixed';
textarea.style.top = '-999px';
textarea.style.left = '-999px';
document.body.appendChild(textarea);
textarea.focus();
textarea.select();
const success = document.execCommand('copy');
document.body.removeChild(textarea);
return success;
} catch (error) {
console.warn('[FB Tracker] execCommand copy fallback failed:', error);
return false;
}
}
/**
* Extract post text from a Facebook post element
*/
function normalizeSelectionText(text) {
if (!text) {
return '';
}
const trimmed = text.trim();
if (!trimmed) {
return '';
}
return trimmed.length > MAX_SELECTION_LENGTH
? trimmed.substring(0, MAX_SELECTION_LENGTH)
: trimmed;
}
function ensurePrimaryPostElement(element) {
if (!element) {
return element;
}
const selectors = [
'div[role="dialog"] article',
'div[role="dialog"] div[aria-posinset]',
'[data-pagelet*="FeedUnit"] article',
'div[role="main"] article',
'[data-visualcompletion="ignore-dynamic"] article',
'div[aria-posinset]',
'article[role="article"]',
'article'
];
if (isOnReelsPage()) {
selectors.unshift('div[role="complementary"]');
}
let current = element;
while (current && current !== document.body && current !== document.documentElement) {
for (const selector of selectors) {
if (current.matches && current.matches(selector)) {
return current;
}
}
current = current.parentElement;
}
return element;
}
function cacheSelectionForPost(postElement) {
try {
const selection = window.getSelection();
if (!selection || selection.rangeCount === 0) {
return;
}
const text = normalizeSelectionText(selection.toString());
if (text) {
postSelectionCache.set(postElement, {
text,
timestamp: Date.now()
});
lastGlobalSelection = { text, timestamp: Date.now() };
}
} catch (error) {
console.warn('[FB Tracker] Failed to cache selection text:', error);
}
}
function cacheCurrentSelection() {
try {
const selection = window.getSelection();
if (!selection || selection.rangeCount === 0) {
return;
}
const text = normalizeSelectionText(selection.toString());
if (text) {
lastGlobalSelection = {
text,
timestamp: Date.now()
};
}
} catch (error) {
console.debug('[FB Tracker] Unable to cache current selection:', error);
}
}
function getSelectedTextFromPost(postElement) {
try {
const selection = window.getSelection();
if (!selection || selection.rangeCount === 0) {
throw new Error('No active selection');
}
const text = normalizeSelectionText(selection.toString());
if (text) {
postSelectionCache.set(postElement, { text, timestamp: Date.now() });
lastGlobalSelection = { text, timestamp: Date.now() };
return text;
}
throw new Error('Empty selection');
} catch (error) {
if (error && error.message) {
console.debug('[FB Tracker] Selection fallback:', error.message);
}
const cached = postSelectionCache.get(postElement);
if (cached && Date.now() - cached.timestamp < LAST_SELECTION_MAX_AGE) {
return cached.text;
}
if (lastGlobalSelection.text && Date.now() - lastGlobalSelection.timestamp < LAST_SELECTION_MAX_AGE) {
return lastGlobalSelection.text;
}
return '';
}
}
function extractPostText(postElement) {
if (!postElement) {
return '';
}
const logPostText = (...args) => {
try {
console.log(POST_TEXT_LOG_TAG, ...args);
} catch (error) {
// ignore logging failure
}
};
const SKIP_TEXT_CONTAINERS_SELECTOR = [
'div[role="textbox"]',
'[contenteditable="true"]',
'[data-lexical-editor="true"]',
'form[role="presentation"]',
'form[method]',
'.fb-tracker-ui',
'.fb-tracker-ai-wrapper',
'[aria-label*="komment"]',
'[aria-label*="comment"]',
'[aria-roledescription*="komment"]',
'[aria-roledescription*="comment"]'
].join(', ');
const KEYWORD_HINTS = ['meta', 'facebook', 'instagram'];
const isInsideSkippedRegion = (element) => {
if (!element || typeof element.closest !== 'function') {
return false;
}
return Boolean(element.closest(SKIP_TEXT_CONTAINERS_SELECTOR));
};
const scoreCandidate = (text) => {
const base = text.length;
const lower = text.toLowerCase();
let bonus = 0;
for (const keyword of KEYWORD_HINTS) {
if (lower.includes(keyword)) {
bonus += 200;
}
}
return base + bonus;
};
const makeSnippet = (text) => {
if (!text) {
return '';
}
const trimmed = text.trim();
return trimmed.length > 140 ? `${trimmed.substring(0, 137)}…` : trimmed;
};
const contentSelectors = [
'[data-ad-preview="message"]',
'[data-ad-comet-preview="message"]',
'div[data-ad-comet-preview] > div > div > span',
'.x193iq5w.xeuugli', // Common Facebook text class
'span[dir="auto"]',
'div[dir="auto"]'
];
const uiTextPattern = /(Gefällt mir|Kommentieren|Teilen|Like|Comment|Share)/gi;
const timePattern = /\d+\s*(Std\.|Min\.|Tag|hour|minute|day)/gi;
const sponsoredPattern = /(Gesponsert|Sponsored)/gi;
const cleanCandidate = (text) => {
if (!text) {
return '';
}
const cleaned = text
.replace(uiTextPattern, ' ')
.replace(timePattern, ' ')
.replace(sponsoredPattern, ' ')
.replace(/\s+/g, ' ')
.trim();
if (!cleaned) {
logPostText('Discard empty candidate after cleaning');
return '';
}
// Ignore very short snippets that are likely button labels
if (cleaned.length < 5 && cleaned.split(/\s+/).length < 2) {
logPostText('Discard very short candidate', makeSnippet(text));
return '';
}
return cleaned;
};
const candidates = [];
const seen = new Set();
const tryAddCandidate = (rawText, element = null, context = {}) => {
const candidate = cleanCandidate(rawText);
if (!candidate) {
if (rawText) {
logPostText('Candidate rejected during cleaning', makeSnippet(rawText), context);
}
return;
}
if (seen.has(candidate)) {
logPostText('Candidate skipped as duplicate', makeSnippet(candidate), context);
return;
}
if (element && isInsideSkippedRegion(element)) {
logPostText('Candidate inside skipped region', makeSnippet(candidate), context);
return;
}
seen.add(candidate);
candidates.push({
text: candidate,
score: scoreCandidate(candidate)
});
logPostText('Candidate accepted', {
score: scoreCandidate(candidate),
snippet: makeSnippet(candidate),
context
});
};
logPostText('Begin extraction');
for (const selector of contentSelectors) {
const elements = postElement.querySelectorAll(selector);
for (const element of elements) {
if (isInsideSkippedRegion(element)) {
logPostText('Selector result skipped (inside disallowed region)', selector, makeSnippet(element.innerText || element.textContent || ''));
continue;
}
tryAddCandidate(element.innerText || element.textContent || '', element, { selector });
}
}
let textContent = '';
if (candidates.length) {
const best = candidates.reduce((top, current) => (
current.score > top.score ? current : top
), candidates[0]);
textContent = best.text;
logPostText('Best candidate selected', {
score: best.score,
snippet: makeSnippet(best.text)
});
}
if (!textContent) {
let fallbackText = '';
try {
const clone = postElement.cloneNode(true);
const elementsToRemove = clone.querySelectorAll(SKIP_TEXT_CONTAINERS_SELECTOR);
elementsToRemove.forEach((node) => node.remove());
const cloneText = clone.innerText || clone.textContent || '';
fallbackText = cleanCandidate(cloneText);
logPostText('Fallback extracted from clone', makeSnippet(fallbackText || cloneText));
} catch (error) {
const allText = postElement.innerText || postElement.textContent || '';
fallbackText = cleanCandidate(allText);
logPostText('Fallback extracted from original element', makeSnippet(fallbackText || allText));
}
textContent = fallbackText;
}
if (!textContent) {
logPostText('No usable text found');
return '';
}
logPostText('Final post text', makeSnippet(textContent));
return textContent.substring(0, 2000); // Limit length
}
/**
* Find and click the comment button to open comment field
*/
function findAndClickCommentButton(postElement) {
if (!postElement) {
return false;
}
// Look for comment button with various selectors
const commentButtonSelectors = [
'[data-ad-rendering-role="comment_button"]',
'[aria-label*="Kommentieren"]',
'[aria-label*="Comment"]'
];
for (const selector of commentButtonSelectors) {
const button = postElement.querySelector(selector);
if (button) {
console.log('[FB Tracker] Found comment button, clicking it');
button.click();
return true;
}
}
// Try in parent elements
let parent = postElement;
for (let i = 0; i < 3; i++) {
parent = parent.parentElement;
if (!parent) break;
for (const selector of commentButtonSelectors) {
const button = parent.querySelector(selector);
if (button) {
console.log('[FB Tracker] Found comment button in parent, clicking it');
button.click();
return true;
}
}
}
return false;
}
function containsPostContent(element) {
if (!element || element.nodeType !== Node.ELEMENT_NODE) {
return false;
}
if (element.matches && element.matches('article, [role="article"]')) {
return true;
}
if (element.querySelector && element.querySelector('article, [role="article"]')) {
return true;
}
if (element.matches && element.matches('[data-fb-tracker-processed="1"]')) {
return true;
}
return false;
}
function isTimestampArtifactNode(node) {
if (!node || node.nodeType !== Node.ELEMENT_NODE) {
return false;
}
if (!node.classList) {
return false;
}
if (!node.classList.contains('__fb-light-mode') && !node.classList.contains('__fb-dark-mode')) {
return false;
}
if (node.querySelector && node.querySelector('article, [role="article"]')) {
return false;
}
const text = node.textContent ? node.textContent.trim() : '';
if (!text) {
return true;
}
if (text.length > 80) {
return false;
}
const lowered = text.toLowerCase();
const hasDate = /\b\d{1,2}\.\s*(?:jan|feb|mär|mae|apr|mai|jun|jul|aug|sep|okt|nov|dez)/i.test(lowered);
const hasTime = /\b\d{1,2}[:.]\d{2}\b/.test(lowered);
const hasMonthWord = /\b(?:januar|februar|märz|maerz|april|mai|juni|juli|august|september|oktober|november|dezember)\b/.test(lowered);
if (hasDate || hasTime || hasMonthWord) {
return true;
}
return false;
}
function removeNodeAndEmptyAncestors(node) {
if (!node || node.nodeType !== Node.ELEMENT_NODE) {
return;
}
const parents = [];
let currentParent = node.parentElement;
node.remove();
while (currentParent && currentParent !== document.body && currentParent !== document.documentElement) {
parents.push(currentParent);
currentParent = currentParent.parentElement;
}
parents.forEach((parentNode) => {
if (parentNode.childElementCount === 0) {
parentNode.remove();
}
});
}
function cleanupDanglingSearchArtifacts(context) {
if (!isOnSearchResultsPage()) {
return;
}
const scope = (context && context.nodeType === Node.ELEMENT_NODE)
? context
: document;
const candidates = scope.querySelectorAll('div.__fb-light-mode, div.__fb-dark-mode');
candidates.forEach((node) => {
if (!isTimestampArtifactNode(node)) {
return;
}
const parent = node.parentElement;
if (parent) {
const hasContentSibling = Array.from(parent.children).some((child) => {
if (child === node) {
return false;
}
return containsPostContent(child);
});
if (hasContentSibling) {
return;
}
}
removeNodeAndEmptyAncestors(node);
});
}
/**
* Find comment input field on current page
*/
function findCommentInput(postElement, options = {}) {
const {
preferredRoot = null,
includeParents = true
} = options;
if (!postElement && !preferredRoot) {
return null;
}
const selectors = [
'div[contenteditable="true"][role="textbox"]',
'div[aria-label*="Kommentar"][contenteditable="true"]',
'div[aria-label*="comment"][contenteditable="true"]',
'div[aria-label*="Write a comment"][contenteditable="true"]'
];
const searchInRoot = (root) => {
if (!root) {
return null;
}
for (const selector of selectors) {
const input = root.querySelector(selector);
if (input && isElementVisible(input)) {
return input;
}
}
return null;
};
const roots = [];
if (postElement) {
roots.push(postElement);
}
if (preferredRoot && preferredRoot.isConnected && !roots.includes(preferredRoot)) {
roots.push(preferredRoot);
}
for (const root of roots) {
const input = searchInRoot(root);
if (input) {
return input;
}
}
if (includeParents && postElement) {
let parent = postElement.parentElement;
for (let i = 0; i < 3 && parent; i++) {
if (preferredRoot && !preferredRoot.contains(parent)) {
parent = parent.parentElement;
continue;
}
const input = searchInRoot(parent);
if (input) {
return input;
}
parent = parent.parentElement;
}
}
return null;
}
function isElementVisible(element) {
if (!element || !element.isConnected) {
return false;
}
if (typeof element.offsetParent !== 'undefined' && element.offsetParent !== null) {
return true;
}
const rects = element.getClientRects();
return rects && rects.length > 0;
}
function isCancellationError(error) {
if (!error) {
return false;
}
if (error.name === 'AICancelled' || error.name === 'AbortError') {
return true;
}
if (typeof error.message === 'string') {
const normalized = error.message.toLowerCase();
if (normalized === 'ai_cancelled' || normalized === 'abgebrochen') {
return true;
}
}
return false;
}
async function waitForCommentInput(postElement, options = {}) {
const {
encodedPostUrl = null,
timeout = 6000,
interval = 200,
context = null,
preferredRoot: rawPreferredRoot = null
} = options;
const deadline = Date.now() + Math.max(timeout, 0);
let attempts = 0;
const preferredRoot = rawPreferredRoot && rawPreferredRoot.isConnected
? rawPreferredRoot
: null;
const findByEncodedUrl = () => {
if (context && context.cancelled) {
return null;
}
if (!encodedPostUrl) {
return null;
}
const trackers = document.querySelectorAll(`.fb-tracker-ui[data-post-url="${encodedPostUrl}"]`);
for (const tracker of trackers) {
if (!tracker.isConnected) {
continue;
}
if (preferredRoot && !preferredRoot.contains(tracker)) {
continue;
}
const trackerContainer = tracker.closest('div[aria-posinset], article[role="article"], article, div[role="complementary"]');
if (trackerContainer) {
const input = findCommentInput(trackerContainer, { preferredRoot });
if (isElementVisible(input)) {
return input;
}
}
const dialogRoot = preferredRoot || tracker.closest(DIALOG_ROOT_SELECTOR);
if (dialogRoot) {
const dialogInput = dialogRoot.querySelector('div[contenteditable="true"][role="textbox"]');
if (isElementVisible(dialogInput)) {
return dialogInput;
}
}
}
return null;
};
while (Date.now() <= deadline) {
if (context && context.cancelled) {
return null;
}
attempts++;
let input = findCommentInput(postElement, { preferredRoot });
if (isElementVisible(input)) {
if (attempts > 1) {
console.log('[FB Tracker] Comment input located after', attempts, 'attempts (post context)');
}
return input;
}
input = findByEncodedUrl();
if (input) {
if (attempts > 1) {
console.log('[FB Tracker] Comment input located after', attempts, 'attempts (encoded URL context)');
}
return input;
}
const dialogRootFromPost = preferredRoot
|| (postElement ? postElement.closest(DIALOG_ROOT_SELECTOR) : null);
if (dialogRootFromPost) {
const dialogInput = dialogRootFromPost.querySelector('div[contenteditable="true"][role="textbox"]');
if (isElementVisible(dialogInput)) {
if (attempts > 1) {
console.log('[FB Tracker] Comment input located after', attempts, 'attempts (dialog context)');
}
return dialogInput;
}
}
if (!preferredRoot) {
const fallbackDialog = document.querySelector(DIALOG_ROOT_SELECTOR);
if (fallbackDialog && fallbackDialog !== dialogRootFromPost) {
const dialogInput = fallbackDialog.querySelector('div[contenteditable="true"][role="textbox"]');
if (isElementVisible(dialogInput)) {
if (attempts > 1) {
console.log('[FB Tracker] Comment input located after', attempts, 'attempts (fallback dialog)');
}
return dialogInput;
}
}
}
const globalInputs = Array.from(document.querySelectorAll('div[contenteditable="true"][role="textbox"]')).filter(isElementVisible);
if (preferredRoot) {
const scopedInputs = globalInputs.filter(input => preferredRoot.contains(input));
if (scopedInputs.length > 0) {
const lastInput = scopedInputs[scopedInputs.length - 1];
if (lastInput) {
if (attempts > 1) {
console.log('[FB Tracker] Comment input located after', attempts, 'attempts (preferred root fallback)');
}
return lastInput;
}
}
}
if (globalInputs.length > 0) {
const lastInput = globalInputs[globalInputs.length - 1];
if (lastInput) {
if (attempts > 1) {
console.log('[FB Tracker] Comment input located after', attempts, 'attempts (global fallback)');
}
return lastInput;
}
}
await delay(interval);
}
console.log('[FB Tracker] Comment input wait timed out after', timeout, 'ms');
return null;
}
/**
* Set text in comment input field
*/
async function setCommentText(inputElement, text, options = {}) {
const { context = null } = options;
const ensureNotCancelled = () => {
if (context && context.cancelled) {
const cancelError = new Error('AI_CANCELLED');
cancelError.name = 'AICancelled';
throw cancelError;
}
};
if (!inputElement || !text) {
return false;
}
try {
ensureNotCancelled();
console.log('[FB Tracker] Setting comment text:', text.substring(0, 50) + '...');
console.log('[FB Tracker] Input element:', inputElement);
// Focus and click to ensure field is active
inputElement.focus();
inputElement.click();
// Small delay to ensure field is ready
await delay(50);
ensureNotCancelled();
// Clear existing content
inputElement.textContent = '';
// Method 1: Try execCommand first (best for Facebook)
const range = document.createRange();
const selection = window.getSelection();
range.selectNodeContents(inputElement);
selection.removeAllRanges();
selection.addRange(range);
const execSuccess = document.execCommand('insertText', false, text);
console.log('[FB Tracker] execCommand result:', execSuccess);
// Wait a bit and check if it worked
await delay(100);
ensureNotCancelled();
let currentContent = inputElement.textContent || inputElement.innerText || '';
console.log('[FB Tracker] Content after execCommand:', currentContent);
// If execCommand didn't work, use direct method
if (!currentContent || currentContent.trim().length === 0) {
console.log('[FB Tracker] execCommand failed, using direct method');
inputElement.textContent = text;
currentContent = text;
}
// Trigger input events
inputElement.dispatchEvent(new InputEvent('input', { bubbles: true, cancelable: true }));
inputElement.dispatchEvent(new Event('change', { bubbles: true }));
// Final verification
ensureNotCancelled();
const finalContent = inputElement.textContent || inputElement.innerText || '';
console.log('[FB Tracker] Final content:', finalContent.substring(0, 50));
return finalContent.length > 0;
} catch (error) {
console.error('[FB Tracker] Failed to set comment text:', error);
return false;
}
}
function sanitizeAIComment(comment) {
if (!comment || typeof comment !== 'string') {
return '';
}
const tempDiv = document.createElement('div');
tempDiv.innerHTML = comment;
const sanitized = tempDiv.textContent || tempDiv.innerText || '';
return sanitized.trim();
}
/**
* Generate AI comment for a post
*/
async function generateAIComment(postText, profileNumber, options = {}) {
const { signal = null, preferredCredentialId = null } = options;
try {
const payload = {
postText,
profileNumber
};
if (typeof preferredCredentialId === 'number') {
payload.preferredCredentialId = preferredCredentialId;
}
const response = await backendFetch(`${API_URL}/ai/generate-comment`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
signal
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || 'Failed to generate comment');
}
const data = await response.json();
const sanitizedComment = sanitizeAIComment(data.comment);
if (!sanitizedComment) {
throw new Error('AI-Antwort enthält keinen gültigen Text');
}
return sanitizedComment;
} catch (error) {
console.error('[FB Tracker] AI comment generation failed:', error);
throw error;
}
}
async function handleSelectionAIRequest(selectionText, sendResponse) {
try {
const normalizedSelection = normalizeSelectionText(selectionText);
if (!normalizedSelection) {
showToast('Keine gültige Auswahl gefunden', 'error');
sendResponse({ success: false, error: 'Keine gültige Auswahl gefunden' });
return;
}
showToast('AI verarbeitet Auswahl...', 'info');
const profileNumber = await getProfileNumber();
const comment = await generateAIComment(normalizedSelection, profileNumber, {});
if (!comment) {
throw new Error('Keine Antwort vom AI-Dienst erhalten');
}
const copied = await copyTextToClipboard(comment);
if (!copied) {
throw new Error('Antwort konnte nicht in die Zwischenablage kopiert werden');
}
showToast('AI-Antwort in die Zwischenablage kopiert', 'success');
sendResponse({ success: true, comment });
} catch (error) {
console.error('[FB Tracker] Selection AI error:', error);
showToast(`❌ ${error.message || 'Fehler bei AI-Anfrage'}`, 'error');
sendResponse({ success: false, error: error.message || 'Unbekannter Fehler' });
}
}
/**
* Check if AI is enabled
*/
async function isAIEnabled() {
try {
const response = await backendFetch(`${API_URL}/ai-settings`);
if (!response.ok) {
return false;
}
const settings = await response.json();
return settings && settings.enabled === 1;
} catch (error) {
console.warn('[FB Tracker] Failed to check AI settings:', error);
return false;
}
}
/**
* Add AI comment button to tracker UI
*/
async function addAICommentButton(container, postElement) {
const aiEnabled = await isAIEnabled();
if (!aiEnabled) {
return;
}
const actionsContainer = ensureTrackerActionsContainer(container);
if (!actionsContainer) {
return;
}
const encodedPostUrl = container && container.getAttribute('data-post-url')
? container.getAttribute('data-post-url')
: null;
const wrapper = document.createElement('div');
wrapper.className = 'fb-tracker-ai-wrapper';
wrapper.style.cssText = `
position: relative;
display: inline-flex;
align-items: stretch;
border-radius: 6px;
overflow: hidden;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.12);
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
transition: transform 0.2s ease, box-shadow 0.2s ease;
`;
const button = document.createElement('button');
button.type = 'button';
button.className = 'fb-tracker-btn fb-tracker-btn-ai';
button.textContent = '✨ AI';
button.title = 'Generiere automatisch einen passenden Kommentar';
button.style.cssText = `
background: transparent;
color: white;
border: none;
padding: 6px 12px;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
cursor: pointer;
font-size: 13px;
font-weight: 600;
flex: 1 1 auto;
border-radius: 0;
transition: background-color 0.2s ease;
`;
const dropdownButton = document.createElement('button');
dropdownButton.type = 'button';
dropdownButton.className = 'fb-tracker-btn fb-tracker-btn-ai-dropdown';
dropdownButton.textContent = '▾';
dropdownButton.title = 'AI auswählen';
dropdownButton.setAttribute('aria-label', 'AI auswählen');
dropdownButton.setAttribute('aria-haspopup', 'menu');
dropdownButton.setAttribute('aria-expanded', 'false');
dropdownButton.style.cssText = `
background: transparent;
color: white;
border: none;
width: 34px;
font-size: 14px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
border-left: 1px solid rgba(255, 255, 255, 0.35);
border-radius: 0;
transition: background-color 0.2s ease;
`;
const dropdown = document.createElement('div');
dropdown.className = 'fb-tracker-ai-dropdown';
dropdown.style.cssText = `
display: none;
min-width: 220px;
background: #ffffff;
border-radius: 8px;
box-shadow: 0 10px 28px rgba(0, 0, 0, 0.18);
z-index: 2147483647;
padding: 6px 0;
`;
wrapper.appendChild(button);
wrapper.appendChild(dropdownButton);
wrapper.appendChild(dropdown);
actionsContainer.appendChild(wrapper);
const baseWrapperShadow = '0 1px 2px rgba(0, 0, 0, 0.12)';
const setHoverState = (active) => {
if (active) {
wrapper.style.transform = 'translateY(-2px)';
wrapper.style.boxShadow = '0 4px 12px rgba(102, 126, 234, 0.4)';
button.style.backgroundColor = 'rgba(255, 255, 255, 0.08)';
dropdownButton.style.backgroundColor = 'rgba(255, 255, 255, 0.08)';
} else {
wrapper.style.transform = 'translateY(0)';
wrapper.style.boxShadow = baseWrapperShadow;
button.style.backgroundColor = 'transparent';
dropdownButton.style.backgroundColor = 'transparent';
}
};
setHoverState(false);
const baseButtonText = button.textContent;
const resolvePostContexts = () => {
const contextCandidate = container.closest('div[aria-posinset], article[role="article"], article, div[role="complementary"]');
const normalizedContext = contextCandidate ? ensurePrimaryPostElement(contextCandidate) : null;
const fallbackContext = postElement ? ensurePrimaryPostElement(postElement) : null;
const postContext = normalizedContext || fallbackContext || contextCandidate || postElement || container;
return { postContext, contextCandidate, fallbackContext, normalizedContext };
};
const resolvePostContext = () => resolvePostContexts().postContext;
const getAdditionalNote = () => {
const context = resolvePostContext();
return context ? (postAdditionalNotes.get(context) || '') : '';
};
let notePreviewElement = null;
let noteClearButton = null;
const truncateNoteForPreview = (note) => {
if (!note) {
return '';
}
return note.length > 120 ? `${note.slice(0, 117)}…` : note;
};
const updateNoteIndicator = () => {
const note = getAdditionalNote();
const hasNote = note.trim().length > 0;
button.dataset.aiOriginalText = hasNote ? `${baseButtonText} ✎` : baseButtonText;
if ((button.dataset.aiState || 'idle') === 'idle' && !button._aiContext) {
button.textContent = button.dataset.aiOriginalText;
}
button.title = hasNote
? 'Generiere automatisch einen passenden Kommentar (mit Zusatzinfo)'
: 'Generiere automatisch einen passenden Kommentar';
};
const updateNotePreview = () => {
updateNoteIndicator();
if (notePreviewElement) {
const note = getAdditionalNote();
notePreviewElement.textContent = note
? `Aktuelle Zusatzinfo: ${truncateNoteForPreview(note)}`
: 'Keine Zusatzinfo gesetzt';
if (noteClearButton) {
const hasNote = note.trim().length > 0;
noteClearButton.disabled = !hasNote;
noteClearButton.style.opacity = hasNote ? '1' : '0.6';
noteClearButton.style.cursor = hasNote ? 'pointer' : 'default';
}
}
};
const setAdditionalNote = (value) => {
const context = resolvePostContext();
if (!context) {
return;
}
const trimmed = (value || '').trim();
if (trimmed) {
postAdditionalNotes.set(context, trimmed);
} else {
postAdditionalNotes.delete(context);
}
updateNotePreview();
};
const maybeActivateHover = () => {
if ((button.dataset.aiState || 'idle') === 'idle') {
setHoverState(true);
}
};
const maybeDeactivateHover = () => {
if (wrapper.classList.contains('fb-tracker-ai-wrapper--open')) {
return;
}
if (button.matches(':hover') || dropdownButton.matches(':hover')) {
return;
}
setHoverState(false);
};
button.addEventListener('mouseenter', maybeActivateHover);
button.addEventListener('mouseleave', maybeDeactivateHover);
dropdownButton.addEventListener('mouseenter', maybeActivateHover);
dropdownButton.addEventListener('mouseleave', maybeDeactivateHover);
button.addEventListener('pointerdown', () => {
const context = resolvePostContext();
const target = context || postElement || container;
cacheSelectionForPost(target);
});
button.dataset.aiState = 'idle';
button.dataset.aiOriginalText = button.textContent;
let dropdownOpen = false;
let dropdownPortalParent = null;
const resolveDropdownPortalParent = () => {
if (dropdownPortalParent && dropdownPortalParent.isConnected) {
return dropdownPortalParent;
}
const candidate = document.body || document.documentElement;
dropdownPortalParent = candidate;
return dropdownPortalParent;
};
const mountDropdownInPortal = () => {
const portalParent = resolveDropdownPortalParent();
if (!portalParent) {
return;
}
if (dropdown.parentElement !== portalParent) {
portalParent.appendChild(dropdown);
}
};
const restoreDropdownToWrapper = () => {
if (dropdown.parentElement !== wrapper) {
wrapper.appendChild(dropdown);
}
};
const closeDropdown = () => {
if (!dropdownOpen) {
return;
}
dropdown.style.display = 'none';
dropdownOpen = false;
dropdownButton.setAttribute('aria-expanded', 'false');
dropdownButton.textContent = '▾';
wrapper.classList.remove('fb-tracker-ai-wrapper--open');
document.removeEventListener('click', handleOutsideClick, true);
document.removeEventListener('keydown', handleKeydown, true);
if (button.matches(':hover') || dropdownButton.matches(':hover')) {
setHoverState(true);
} else {
setHoverState(false);
}
dropdown.style.position = '';
dropdown.style.top = '';
dropdown.style.left = '';
dropdown.style.maxHeight = '';
dropdown.style.overflowY = '';
restoreDropdownToWrapper();
window.removeEventListener('scroll', repositionDropdown, true);
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();
}
};
const handleKeydown = (event) => {
if (event.key === 'Escape') {
closeDropdown();
}
};
const renderDropdownItems = async () => {
dropdown.innerHTML = '';
const loading = document.createElement('div');
loading.textContent = 'Lade AI-Auswahl...';
loading.style.cssText = 'padding: 8px 14px; font-size: 13px; color: #555;';
dropdown.appendChild(loading);
const appendNoteUI = () => {
noteClearButton = null;
const noteSection = document.createElement('div');
noteSection.style.cssText = 'padding: 10px 14px; display: flex; flex-direction: column; gap: 6px;';
notePreviewElement = document.createElement('div');
notePreviewElement.style.cssText = 'font-size: 12px; color: #65676b; white-space: normal;';
noteSection.appendChild(notePreviewElement);
const buttonsRow = document.createElement('div');
buttonsRow.style.cssText = 'display: flex; gap: 8px; flex-wrap: wrap;';
const editButton = document.createElement('button');
editButton.type = 'button';
editButton.textContent = 'Zusatzinfo bearbeiten';
editButton.style.cssText = 'padding: 6px 10px; border-radius: 6px; border: 1px solid #ccd0d5; background: #fff; cursor: pointer; font-size: 12px; font-weight: 600; color: #1d2129;';
editButton.addEventListener('click', (event) => {
event.preventDefault();
event.stopPropagation();
const existingNote = getAdditionalNote();
const input = window.prompt('Zusatzinfo für den AI-Prompt eingeben:', existingNote);
if (input === null) {
return;
}
const trimmed = (input || '').trim();
setAdditionalNote(trimmed);
if (trimmed) {
showToast('Zusatzinfo gespeichert', 'success');
} else {
showToast('Zusatzinfo entfernt', 'success');
}
});
buttonsRow.appendChild(editButton);
const clearButton = document.createElement('button');
clearButton.type = 'button';
clearButton.textContent = 'Zurücksetzen';
clearButton.style.cssText = 'padding: 6px 10px; border-radius: 6px; border: 1px solid #ccd0d5; background: #fff; cursor: pointer; font-size: 12px; font-weight: 600; color: #a11900;';
clearButton.addEventListener('click', (event) => {
event.preventDefault();
event.stopPropagation();
if (!getAdditionalNote()) {
return;
}
setAdditionalNote('');
showToast('Zusatzinfo entfernt', 'success');
});
buttonsRow.appendChild(clearButton);
noteClearButton = clearButton;
noteSection.appendChild(buttonsRow);
dropdown.appendChild(noteSection);
updateNotePreview();
};
try {
const credentials = await fetchActiveAICredentials();
dropdown.innerHTML = '';
appendNoteUI();
if (!credentials || credentials.length === 0) {
const empty = document.createElement('div');
empty.textContent = 'Keine aktiven AI-Anbieter gefunden';
empty.style.cssText = 'padding: 8px 14px; font-size: 13px; color: #888;';
dropdown.appendChild(empty);
} else {
const divider = document.createElement('div');
divider.style.cssText = 'border-top: 1px solid #e4e6eb; margin: 6px 0;';
dropdown.appendChild(divider);
credentials.forEach((credential) => {
const option = document.createElement('button');
option.type = 'button';
option.className = 'fb-tracker-ai-option';
option.style.cssText = `
width: 100%;
padding: 8px 14px;
background: transparent;
border: none;
text-align: left;
cursor: pointer;
font-size: 13px;
display: flex;
flex-direction: column;
gap: 2px;
`;
option.addEventListener('mouseenter', () => {
option.style.background = '#f0f2f5';
});
option.addEventListener('mouseleave', () => {
option.style.background = 'transparent';
});
const label = document.createElement('span');
label.textContent = formatAICredentialLabel(credential);
label.style.cssText = 'font-weight: 600; color: #1d2129;';
const metaParts = [];
if (credential.provider) {
metaParts.push(`Provider: ${credential.provider}`);
}
if (credential.model) {
metaParts.push(`Modell: ${credential.model}`);
}
if (metaParts.length > 0) {
const meta = document.createElement('span');
meta.textContent = metaParts.join(' · ');
meta.style.cssText = 'font-size: 12px; color: #65676b;';
option.appendChild(label);
option.appendChild(meta);
} else {
option.appendChild(label);
}
option.addEventListener('click', () => {
closeDropdown();
if ((button.dataset.aiState || 'idle') === 'idle') {
cacheSelectionForPost(postElement);
startAIFlow(credential.id);
}
});
dropdown.appendChild(option);
});
}
} catch (error) {
dropdown.innerHTML = '';
appendNoteUI();
const errorItem = document.createElement('div');
errorItem.textContent = error.message || 'AI-Anbieter konnten nicht geladen werden';
errorItem.style.cssText = 'padding: 8px 14px; font-size: 13px; color: #c0392b; white-space: normal;';
dropdown.appendChild(errorItem);
}
};
const positionDropdown = () => {
if (!dropdownOpen) {
return;
}
mountDropdownInPortal();
dropdown.style.position = 'fixed';
dropdown.style.maxHeight = `${Math.max(200, Math.floor(window.innerHeight * 0.6))}px`;
dropdown.style.overflowY = 'auto';
const rect = wrapper.getBoundingClientRect();
const dropdownRect = dropdown.getBoundingClientRect();
const margin = 8;
let top = rect.top - dropdownRect.height - margin;
if (top < margin) {
top = rect.bottom + margin;
}
const viewportPadding = 8;
let left = rect.right - dropdownRect.width;
if (left < viewportPadding) {
left = viewportPadding;
}
const maxLeft = window.innerWidth - dropdownRect.width - viewportPadding;
if (left > maxLeft) {
left = Math.max(viewportPadding, maxLeft);
}
const maxTop = window.innerHeight - dropdownRect.height - margin;
if (top > maxTop) {
top = Math.max(viewportPadding, maxTop);
}
dropdown.style.top = `${top}px`;
dropdown.style.left = `${left}px`;
};
const repositionDropdown = () => {
if (dropdownOpen) {
positionDropdown();
}
};
const toggleDropdown = async () => {
if ((button.dataset.aiState || 'idle') !== 'idle') {
return;
}
if (dropdownOpen) {
closeDropdown();
return;
}
dropdownOpen = true;
wrapper.classList.add('fb-tracker-ai-wrapper--open');
dropdownButton.textContent = '▴';
setHoverState(true);
mountDropdownInPortal();
dropdown.style.display = 'block';
dropdownButton.setAttribute('aria-expanded', 'true');
document.addEventListener('click', handleOutsideClick, true);
document.addEventListener('keydown', handleKeydown, true);
await renderDropdownItems();
positionDropdown();
window.addEventListener('scroll', repositionDropdown, true);
window.addEventListener('resize', repositionDropdown);
};
dropdownButton.addEventListener('click', (event) => {
event.preventDefault();
event.stopPropagation();
toggleDropdown();
});
const startAIFlow = async (preferredCredentialId = null) => {
closeDropdown();
const originalText = button.dataset.aiOriginalText || '✨ AI';
const currentState = button.dataset.aiState || 'idle';
if (currentState === 'processing') {
const runningContext = button._aiContext;
if (runningContext && !runningContext.cancelled) {
runningContext.cancel();
button.dataset.aiState = 'cancelling';
button.textContent = '✋ Abbruch...';
button.setAttribute('aria-busy', 'true');
button.style.cursor = 'progress';
dropdownButton.style.cursor = 'progress';
button.classList.remove('fb-tracker-btn-ai--processing');
button.classList.add('fb-tracker-btn-ai--cancelling');
}
return;
}
if (currentState === 'cancelling') {
return;
}
const aiContext = {
cancelled: false,
abortController: new AbortController(),
cancel() {
if (!this.cancelled) {
this.cancelled = true;
this.abortController.abort();
}
}
};
const throwIfCancelled = () => {
if (aiContext.cancelled) {
const cancelError = new Error('AI_CANCELLED');
cancelError.name = 'AICancelled';
throw cancelError;
}
};
const updateProcessingText = (text) => {
if (button.dataset.aiState === 'processing' && !aiContext.cancelled) {
button.textContent = text;
}
};
const restoreIdle = (text, revertDelay = 0) => {
button.dataset.aiState = 'idle';
button._aiContext = null;
button.removeAttribute('aria-busy');
button.classList.remove('fb-tracker-btn-ai--processing', 'fb-tracker-btn-ai--cancelling');
button.style.cursor = 'pointer';
dropdownButton.disabled = false;
dropdownButton.style.opacity = '1';
dropdownButton.style.cursor = 'pointer';
dropdownButton.textContent = '▾';
dropdownButton.setAttribute('aria-busy', 'false');
button.textContent = text;
if (revertDelay > 0) {
setTimeout(() => {
if ((button.dataset.aiState || 'idle') === 'idle' && !button._aiContext) {
button.textContent = originalText;
}
}, revertDelay);
}
if (wrapper.classList.contains('fb-tracker-ai-wrapper--open')) {
setHoverState(true);
} else if (!(button.matches(':hover') || dropdownButton.matches(':hover'))) {
setHoverState(false);
}
};
button._aiContext = aiContext;
button.dataset.aiState = 'processing';
button.setAttribute('aria-busy', 'true');
button.classList.add('fb-tracker-btn-ai--processing');
button.classList.remove('fb-tracker-btn-ai--cancelling');
button.style.cursor = 'progress';
setHoverState(true);
dropdownButton.disabled = true;
dropdownButton.style.opacity = '0.5';
dropdownButton.style.cursor = 'not-allowed';
dropdownButton.setAttribute('aria-busy', 'true');
button.textContent = '⏳ Generiere...';
try {
const contexts = resolvePostContexts();
const { postContext, contextCandidate, fallbackContext } = contexts;
const selectionKeys = [];
if (postContext) {
selectionKeys.push(postContext);
}
if (postElement && postElement !== postContext) {
selectionKeys.push(postElement);
}
if (contextCandidate && contextCandidate !== postContext && contextCandidate !== postElement) {
selectionKeys.push(contextCandidate);
}
if (fallbackContext
&& fallbackContext !== postContext
&& fallbackContext !== postElement
&& fallbackContext !== contextCandidate) {
selectionKeys.push(fallbackContext);
}
const resolveRecentSelection = () => {
for (const key of selectionKeys) {
if (!key) {
continue;
}
const entry = postSelectionCache.get(key);
if (entry && Date.now() - entry.timestamp < LAST_SELECTION_MAX_AGE) {
return entry;
}
}
return null;
};
let postText = '';
const cachedSelection = resolveRecentSelection();
if (cachedSelection) {
console.log('[FB Tracker] Using cached selection text');
postText = cachedSelection.text;
}
throwIfCancelled();
if (!postText) {
const selectionSource = postContext || postElement;
if (selectionSource) {
postText = getSelectedTextFromPost(selectionSource);
if (postText) {
console.log('[FB Tracker] Using active selection text');
}
}
}
if (!postText) {
const latestCached = resolveRecentSelection();
if (latestCached) {
console.log('[FB Tracker] Using latest cached selection after check');
postText = latestCached.text;
}
}
if (!postText) {
postText = extractPostText(postContext);
if (postText) {
console.log('[FB Tracker] Fallback to DOM extraction');
}
}
if (!postText) {
throw new Error('Konnte Post-Text nicht extrahieren');
}
selectionKeys.forEach((key) => {
if (key) {
postSelectionCache.delete(key);
}
});
const additionalNote = postContext ? (postAdditionalNotes.get(postContext) || '') : '';
if (additionalNote) {
postText = `${postText}\n\nZusatzinfo:\n${additionalNote}`;
}
throwIfCancelled();
console.log('[FB Tracker] Generating AI comment for:', postText.substring(0, 100));
const profileNumber = await getProfileNumber();
throwIfCancelled();
const comment = await generateAIComment(postText, profileNumber, {
signal: aiContext.abortController.signal,
preferredCredentialId
});
throwIfCancelled();
console.log('[FB Tracker] Generated comment:', comment);
const dialogRoot = container.closest(DIALOG_ROOT_SELECTOR)
|| (postContext && postContext.closest(DIALOG_ROOT_SELECTOR));
let commentInput = findCommentInput(postContext, { preferredRoot: dialogRoot });
let waitedForInput = false;
if (!commentInput) {
console.log('[FB Tracker] Comment input not found, trying to click comment button');
let buttonClicked = findAndClickCommentButton(postContext);
if (!buttonClicked && dialogRoot) {
const dialogCommentButton = dialogRoot.querySelector('[data-ad-rendering-role="comment_button"], [aria-label*="Kommentieren"], [aria-label*="Comment"]');
if (dialogCommentButton && isElementVisible(dialogCommentButton)) {
dialogCommentButton.click();
buttonClicked = true;
}
}
updateProcessingText(buttonClicked ? '⏳ Öffne Kommentare...' : '⏳ Suche Kommentarfeld...');
waitedForInput = true;
commentInput = await waitForCommentInput(postContext, {
encodedPostUrl,
timeout: buttonClicked ? 8000 : 5000,
interval: 250,
context: aiContext,
preferredRoot: dialogRoot
});
}
if (!commentInput && !waitedForInput) {
updateProcessingText('⏳ Suche Kommentarfeld...');
waitedForInput = true;
commentInput = await waitForCommentInput(postContext, {
encodedPostUrl,
timeout: 4000,
interval: 200,
context: aiContext,
preferredRoot: dialogRoot
});
}
throwIfCancelled();
if (!commentInput) {
throwIfCancelled();
await navigator.clipboard.writeText(comment);
throwIfCancelled();
showToast('📋 Kommentarfeld nicht gefunden - In Zwischenablage kopiert', 'info');
restoreIdle('📋 Kopiert', 2000);
await confirmParticipationAfterAI(profileNumber);
return;
}
if (waitedForInput) {
updateProcessingText('⏳ Füge Kommentar ein...');
}
const success = await setCommentText(commentInput, comment, { context: aiContext });
throwIfCancelled();
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);
if (cancelled) {
console.log('[FB Tracker] AI comment operation cancelled');
restoreIdle('✋ Abgebrochen', 1500);
showToast('⏹️ Vorgang abgebrochen', 'info');
return;
}
console.error('[FB Tracker] AI comment error:', error);
showToast(`❌ ${error.message}`, 'error');
restoreIdle(originalText);
}
};
button.addEventListener('click', (event) => {
event.preventDefault();
event.stopPropagation();
startAIFlow();
});
container.appendChild(wrapper);
}
// Expose function globally so it can be called from createTrackerUI
window.addAICommentButton = addAICommentButton;