aktueller stand

This commit is contained in:
2025-12-29 19:45:08 +01:00
parent fde5ab91c8
commit 677eac2632
6 changed files with 1888 additions and 272 deletions

View File

@@ -300,6 +300,14 @@ const includeExpiredToggle = document.getElementById('includeExpiredToggle');
const mergeControls = document.getElementById('mergeControls');
const mergeModeToggle = document.getElementById('mergeModeToggle');
const mergeSubmitBtn = document.getElementById('mergeSubmitBtn');
const pendingBulkControls = document.getElementById('pendingBulkControls');
const pendingBulkCountSelect = document.getElementById('pendingBulkCountSelect');
const pendingBulkOpenBtn = document.getElementById('pendingBulkOpenBtn');
const pendingAutoOpenToggle = document.getElementById('pendingAutoOpenToggle');
const pendingAutoOpenOverlay = document.getElementById('pendingAutoOpenOverlay');
const pendingAutoOpenOverlayPanel = document.getElementById('pendingAutoOpenOverlayPanel');
const pendingAutoOpenCountdown = document.getElementById('pendingAutoOpenCountdown');
const pendingBulkStatus = document.getElementById('pendingBulkStatus');
const REFRESH_SETTINGS_KEY = 'trackerRefreshSettings';
const SORT_SETTINGS_KEY = 'trackerSortSettings';
@@ -313,6 +321,12 @@ const BOOKMARK_WINDOW_DAYS = 28;
const BOOKMARK_SUFFIXES = ['Gewinnspiel', 'gewinnen', 'verlosen'];
const BOOKMARK_PREFS_KEY = 'trackerBookmarkPreferences';
const INCLUDE_EXPIRED_STORAGE_KEY = 'trackerIncludeExpired';
const PENDING_BULK_COUNT_STORAGE_KEY = 'trackerPendingBulkCount';
const PENDING_AUTO_OPEN_STORAGE_KEY = 'trackerPendingAutoOpen';
const DEFAULT_PENDING_BULK_COUNT = 5;
const PENDING_AUTO_OPEN_DELAY_MS = 1500;
const PENDING_OPEN_COOLDOWN_STORAGE_KEY = 'trackerPendingOpenCooldown';
const PENDING_OPEN_COOLDOWN_MS = 40 * 60 * 1000;
function loadIncludeExpiredPreference() {
try {
@@ -337,6 +351,110 @@ function persistIncludeExpiredPreference(value) {
}
}
function getPendingOpenCooldownStorageKey(profileNumber = currentProfile) {
const safeProfile = profileNumber || currentProfile || 1;
return `${PENDING_OPEN_COOLDOWN_STORAGE_KEY}:${safeProfile}`;
}
function loadPendingOpenCooldownMap(profileNumber = currentProfile) {
const storageKey = getPendingOpenCooldownStorageKey(profileNumber);
try {
const raw = localStorage.getItem(storageKey);
if (raw) {
const parsed = JSON.parse(raw);
if (parsed && typeof parsed === 'object') {
const now = Date.now();
const cleaned = {};
Object.entries(parsed).forEach(([id, timestamp]) => {
const value = Number(timestamp);
if (Number.isFinite(value) && now - value < PENDING_OPEN_COOLDOWN_MS) {
cleaned[id] = value;
}
});
if (Object.keys(cleaned).length !== Object.keys(parsed).length) {
localStorage.setItem(storageKey, JSON.stringify(cleaned));
}
return cleaned;
}
}
} catch (error) {
console.warn('Konnte Cooldown-Daten für offene Beiträge nicht laden:', error);
}
return {};
}
function persistPendingOpenCooldownMap(profileNumber, map) {
const storageKey = getPendingOpenCooldownStorageKey(profileNumber);
try {
localStorage.setItem(storageKey, JSON.stringify(map || {}));
} catch (error) {
console.warn('Konnte Cooldown-Daten für offene Beiträge nicht speichern:', error);
}
}
function isPendingOpenCooldownActive(postId) {
if (!postId) {
return false;
}
const timestamp = pendingOpenCooldownMap[postId];
if (!timestamp) {
return false;
}
const elapsed = Date.now() - timestamp;
if (elapsed < PENDING_OPEN_COOLDOWN_MS) {
return true;
}
delete pendingOpenCooldownMap[postId];
persistPendingOpenCooldownMap(currentProfile, pendingOpenCooldownMap);
return false;
}
function recordPendingOpen(postId) {
if (!postId) {
return;
}
pendingOpenCooldownMap[postId] = Date.now();
persistPendingOpenCooldownMap(currentProfile, pendingOpenCooldownMap);
}
function loadPendingBulkCount() {
try {
const stored = localStorage.getItem(PENDING_BULK_COUNT_STORAGE_KEY);
const value = parseInt(stored, 10);
if (!Number.isNaN(value) && [1, 5, 10, 15, 20].includes(value)) {
return value;
}
} catch (error) {
console.warn('Konnte Bulk-Anzahl für offene Beiträge nicht laden:', error);
}
return DEFAULT_PENDING_BULK_COUNT;
}
function persistPendingBulkCount(value) {
try {
localStorage.setItem(PENDING_BULK_COUNT_STORAGE_KEY, String(value));
} catch (error) {
console.warn('Konnte Bulk-Anzahl für offene Beiträge nicht speichern:', error);
}
}
function loadPendingAutoOpenEnabled() {
try {
return localStorage.getItem(PENDING_AUTO_OPEN_STORAGE_KEY) === '1';
} catch (error) {
console.warn('Konnte Auto-Öffnen-Status nicht laden:', error);
return false;
}
}
function persistPendingAutoOpenEnabled(enabled) {
try {
localStorage.setItem(PENDING_AUTO_OPEN_STORAGE_KEY, enabled ? '1' : '0');
} catch (error) {
console.warn('Konnte Auto-Öffnen-Status nicht speichern:', error);
}
}
function updateIncludeExpiredToggleUI() {
if (!includeExpiredToggle) {
return;
@@ -345,6 +463,13 @@ function updateIncludeExpiredToggleUI() {
}
includeExpiredPosts = loadIncludeExpiredPreference();
let pendingBulkCount = loadPendingBulkCount();
let pendingAutoOpenEnabled = loadPendingAutoOpenEnabled();
let pendingAutoOpenTriggered = false;
let pendingAutoOpenTimerId = null;
let pendingAutoOpenCountdownIntervalId = null;
let pendingProcessingBatch = false;
let pendingOpenCooldownMap = loadPendingOpenCooldownMap(currentProfile);
function updateIncludeExpiredToggleVisibility() {
if (!includeExpiredToggle) {
@@ -388,6 +513,26 @@ function updateMergeControlsUI() {
}
}
function updatePendingBulkControls(filteredCount = 0) {
if (!pendingBulkControls) {
return;
}
const isPendingTab = currentTab === 'pending';
pendingBulkControls.hidden = !isPendingTab;
pendingBulkControls.style.display = isPendingTab ? 'flex' : 'none';
if (pendingBulkOpenBtn) {
pendingBulkOpenBtn.disabled = !isPendingTab || pendingProcessingBatch || filteredCount === 0;
}
}
function setPendingBulkStatus(message = '', isError = false) {
if (!pendingBulkStatus) {
return;
}
pendingBulkStatus.textContent = message || '';
pendingBulkStatus.classList.toggle('bulk-status--error', !!isError);
}
function initializeFocusParams() {
try {
const params = new URLSearchParams(window.location.search);
@@ -1635,6 +1780,16 @@ function updateSortDirectionToggleUI() {
}
}
function getDefaultSortDirectionForMode(mode) {
if (mode === 'deadline') {
return 'asc';
}
if (mode === 'smart') {
return 'desc';
}
return null;
}
function normalizeRequiredProfiles(post) {
if (Array.isArray(post.required_profiles) && post.required_profiles.length) {
return post.required_profiles
@@ -1714,6 +1869,224 @@ function updateFilteredCount(tab, count) {
tabFilteredCounts[key] = Math.max(0, count || 0);
}
function getPostListState() {
const postItems = posts.map((post) => ({
post,
status: computePostStatus(post)
}));
const sortedItems = [...postItems].sort(comparePostItems);
let filteredItems = sortedItems;
if (currentTab === 'pending') {
filteredItems = sortedItems.filter((item) => !item.status.isExpired && item.status.canCurrentProfileCheck && !item.status.isComplete);
} else {
filteredItems = includeExpiredPosts
? sortedItems
: sortedItems.filter((item) => !item.status.isExpired && !item.status.isComplete);
}
const tabTotalCount = filteredItems.length;
const searchInput = document.getElementById('searchInput');
const searchValue = searchInput && typeof searchInput.value === 'string' ? searchInput.value.trim() : '';
const searchActive = Boolean(searchValue);
if (searchActive) {
const searchTerm = searchValue.toLowerCase();
filteredItems = filteredItems.filter((item) => {
const post = item.post;
return (
(post.title && post.title.toLowerCase().includes(searchTerm)) ||
(post.url && post.url.toLowerCase().includes(searchTerm)) ||
(post.created_by_name && post.created_by_name.toLowerCase().includes(searchTerm)) ||
(post.id && post.id.toLowerCase().includes(searchTerm))
);
});
}
return {
sortedItems,
filteredItems,
tabTotalCount,
searchActive,
searchValue
};
}
function clearPendingAutoOpenCountdown() {
if (pendingAutoOpenCountdownIntervalId) {
clearInterval(pendingAutoOpenCountdownIntervalId);
pendingAutoOpenCountdownIntervalId = null;
}
}
function updatePendingAutoOpenCountdownLabel(remainingMs) {
if (!pendingAutoOpenCountdown) {
return;
}
const safeMs = Math.max(0, remainingMs);
const seconds = safeMs / 1000;
const formatted = seconds >= 10 ? seconds.toFixed(0) : seconds.toFixed(1);
pendingAutoOpenCountdown.textContent = formatted;
}
function hidePendingAutoOpenOverlay() {
clearPendingAutoOpenCountdown();
if (pendingAutoOpenOverlay) {
pendingAutoOpenOverlay.classList.remove('visible');
pendingAutoOpenOverlay.hidden = true;
}
}
function showPendingAutoOpenOverlay(delayMs) {
if (!pendingAutoOpenOverlay) {
return;
}
const duration = Math.max(0, delayMs);
hidePendingAutoOpenOverlay();
pendingAutoOpenOverlay.hidden = false;
requestAnimationFrame(() => pendingAutoOpenOverlay.classList.add('visible'));
updatePendingAutoOpenCountdownLabel(duration);
const start = Date.now();
pendingAutoOpenCountdownIntervalId = setInterval(() => {
const remaining = Math.max(0, duration - (Date.now() - start));
updatePendingAutoOpenCountdownLabel(remaining);
if (remaining <= 0) {
clearPendingAutoOpenCountdown();
}
}, 100);
}
function cancelPendingAutoOpen(showMessage = false) {
if (pendingAutoOpenTimerId) {
clearTimeout(pendingAutoOpenTimerId);
pendingAutoOpenTimerId = null;
}
pendingAutoOpenTriggered = false;
hidePendingAutoOpenOverlay();
if (showMessage) {
setPendingBulkStatus('Automatisches Öffnen abgebrochen.', false);
}
}
function getPendingVisibleCandidates() {
if (currentTab !== 'pending') {
return { items: [], totalVisible: 0, cooldownBlocked: 0 };
}
const { filteredItems } = getPostListState();
const visibleCount = Math.min(filteredItems.length, getVisibleCount(currentTab));
const visibleItems = filteredItems
.slice(0, visibleCount)
.filter(({ post }) => post && post.url);
const items = visibleItems.filter(({ post }) => !isPendingOpenCooldownActive(post.id));
const cooldownBlocked = Math.max(0, visibleItems.length - items.length);
return { items, totalVisible: visibleItems.length, cooldownBlocked };
}
function openPendingBatch({ auto = false } = {}) {
if (pendingProcessingBatch) {
return;
}
if (!auto) {
cancelPendingAutoOpen(false);
}
const { items: candidates, totalVisible, cooldownBlocked } = getPendingVisibleCandidates();
if (!candidates.length) {
if (!auto) {
if (totalVisible === 0) {
setPendingBulkStatus('Keine offenen Beiträge zum Öffnen.', true);
} else if (cooldownBlocked > 0) {
setPendingBulkStatus('Alle sichtbaren Beiträge sind noch im Cooldown (40 min).', true);
} else {
setPendingBulkStatus('Keine offenen Beiträge zum Öffnen.', true);
}
}
pendingAutoOpenTriggered = false;
return;
}
const count = pendingBulkCount || DEFAULT_PENDING_BULK_COUNT;
const selection = candidates.slice(0, count);
pendingProcessingBatch = true;
if (pendingBulkOpenBtn) {
pendingBulkOpenBtn.disabled = true;
}
if (!auto) {
setPendingBulkStatus('');
} else {
setPendingBulkStatus(`Öffne automatisch ${selection.length} Links...`, false);
}
selection.forEach(({ post }) => {
if (post && post.url) {
window.open(post.url, '_blank', 'noopener');
recordPendingOpen(post.id);
}
});
pendingProcessingBatch = false;
if (pendingBulkOpenBtn) {
pendingBulkOpenBtn.disabled = false;
}
if (auto) {
setPendingBulkStatus('');
pendingAutoOpenTriggered = false;
}
}
function maybeAutoOpenPending(reason = '', delayMs = PENDING_AUTO_OPEN_DELAY_MS) {
if (!isPostsViewActive()) {
hidePendingAutoOpenOverlay();
return;
}
if (currentTab !== 'pending') {
hidePendingAutoOpenOverlay();
return;
}
if (!pendingAutoOpenEnabled) {
hidePendingAutoOpenOverlay();
return;
}
if (pendingProcessingBatch) {
return;
}
if (pendingAutoOpenTriggered) {
return;
}
const { items: candidates } = getPendingVisibleCandidates();
if (!candidates.length) {
hidePendingAutoOpenOverlay();
return;
}
if (pendingAutoOpenTimerId) {
clearTimeout(pendingAutoOpenTimerId);
pendingAutoOpenTimerId = null;
}
hidePendingAutoOpenOverlay();
pendingAutoOpenTriggered = true;
const delay = typeof delayMs === 'number' ? Math.max(0, delayMs) : PENDING_AUTO_OPEN_DELAY_MS;
if (delay === 0) {
if (pendingAutoOpenEnabled) {
openPendingBatch({ auto: true });
} else {
pendingAutoOpenTriggered = false;
}
return;
}
showPendingAutoOpenOverlay(delay);
pendingAutoOpenTimerId = setTimeout(() => {
pendingAutoOpenTimerId = null;
hidePendingAutoOpenOverlay();
if (pendingAutoOpenEnabled) {
openPendingBatch({ auto: true });
} else {
pendingAutoOpenTriggered = false;
}
}, delay);
}
function cleanupLoadMoreObserver() {
if (loadMoreObserver && observedLoadMoreElement) {
loadMoreObserver.unobserve(observedLoadMoreElement);
@@ -1828,6 +2201,7 @@ function setTab(tab, { updateUrl = true } = {}) {
updateTabInUrl();
}
renderPosts();
maybeAutoOpenPending('tab');
}
function initializeTabFromUrl() {
@@ -3011,7 +3385,9 @@ function applyProfileNumber(profileNumber, { fromBackend = false } = {}) {
}
resetVisibleCount();
pendingOpenCooldownMap = loadPendingOpenCooldownMap(currentProfile);
renderPosts();
maybeAutoOpenPending('profile');
}
// Load profile from localStorage
@@ -3066,6 +3442,42 @@ if (includeExpiredToggle) {
});
}
if (pendingBulkCountSelect) {
pendingBulkCountSelect.value = String(pendingBulkCount);
pendingBulkCountSelect.addEventListener('change', () => {
const value = parseInt(pendingBulkCountSelect.value, 10);
if (!Number.isNaN(value)) {
pendingBulkCount = value;
persistPendingBulkCount(value);
}
});
}
if (pendingBulkOpenBtn) {
pendingBulkOpenBtn.addEventListener('click', () => openPendingBatch());
}
if (pendingAutoOpenOverlayPanel) {
pendingAutoOpenOverlayPanel.addEventListener('click', () => cancelPendingAutoOpen(true));
}
if (pendingAutoOpenToggle) {
pendingAutoOpenToggle.checked = !!pendingAutoOpenEnabled;
pendingAutoOpenToggle.addEventListener('change', () => {
pendingAutoOpenEnabled = pendingAutoOpenToggle.checked;
persistPendingAutoOpenEnabled(pendingAutoOpenEnabled);
pendingAutoOpenTriggered = false;
if (!pendingAutoOpenEnabled && pendingAutoOpenTimerId) {
cancelPendingAutoOpen(false);
}
if (pendingAutoOpenEnabled) {
maybeAutoOpenPending('toggle');
} else {
hidePendingAutoOpenOverlay();
}
});
}
// Tab switching
document.querySelectorAll('.tab-btn').forEach(btn => {
btn.addEventListener('click', () => {
@@ -3164,6 +3576,11 @@ if (sortModeSelect) {
sortModeSelect.addEventListener('change', () => {
const value = sortModeSelect.value;
sortMode = VALID_SORT_MODES.has(value) ? value : DEFAULT_SORT_SETTINGS.mode;
const defaultDirection = getDefaultSortDirectionForMode(sortMode);
if (defaultDirection) {
sortDirection = defaultDirection;
updateSortDirectionToggleUI();
}
saveSortMode();
resetVisibleCount();
renderPosts();
@@ -3201,6 +3618,7 @@ async function fetchPosts({ showLoader = true } = {}) {
}
isFetchingPosts = true;
cancelPendingAutoOpen(false);
try {
if (showLoader) {
@@ -3218,6 +3636,7 @@ async function fetchPosts({ showLoader = true } = {}) {
await normalizeLoadedPostUrls();
sortPostsByCreatedAt();
renderPosts();
maybeAutoOpenPending('load');
} catch (error) {
if (showLoader) {
showError('Fehler beim Laden der Beiträge. Stelle sicher, dass das Backend läuft.');
@@ -3487,45 +3906,18 @@ function renderPosts() {
updateTabButtons();
cleanupLoadMoreObserver();
const postItems = posts.map((post) => ({
post,
status: computePostStatus(post)
}));
const {
sortedItems,
filteredItems: filteredItemsResult,
tabTotalCount,
searchActive
} = getPostListState();
const sortedItems = [...postItems].sort(comparePostItems);
let filteredItems = filteredItemsResult;
const focusCandidateEntry = (!focusHandled && (focusPostIdParam || focusNormalizedUrl))
? sortedItems.find((item) => doesPostMatchFocus(item.post))
: null;
let filteredItems = sortedItems;
if (currentTab === 'pending') {
filteredItems = sortedItems.filter((item) => !item.status.isExpired && item.status.canCurrentProfileCheck && !item.status.isComplete);
} else {
filteredItems = includeExpiredPosts
? sortedItems
: sortedItems.filter((item) => !item.status.isExpired && !item.status.isComplete);
}
const tabTotalCount = filteredItems.length;
const searchInput = document.getElementById('searchInput');
const searchValue = searchInput && typeof searchInput.value === 'string' ? searchInput.value.trim() : '';
const searchActive = Boolean(searchValue);
if (searchActive) {
const searchTerm = searchValue.toLowerCase();
filteredItems = filteredItems.filter((item) => {
const post = item.post;
return (
(post.title && post.title.toLowerCase().includes(searchTerm)) ||
(post.url && post.url.toLowerCase().includes(searchTerm)) ||
(post.created_by_name && post.created_by_name.toLowerCase().includes(searchTerm)) ||
(post.id && post.id.toLowerCase().includes(searchTerm))
);
});
}
if (!focusHandled && focusCandidateEntry && !searchActive) {
const candidateVisibleInCurrentTab = filteredItems.some(({ post }) => doesPostMatchFocus(post));
if (!candidateVisibleInCurrentTab) {
@@ -3554,6 +3946,7 @@ function renderPosts() {
}
updateFilteredCount(currentTab, filteredItems.length);
updatePendingBulkControls(filteredItems.length);
const visibleCount = Math.min(filteredItems.length, getVisibleCount(currentTab));
const visibleItems = filteredItems.slice(0, visibleCount);
@@ -4597,6 +4990,26 @@ window.addEventListener('resize', () => {
}
});
window.addEventListener('app:view-change', (event) => {
const view = event && event.detail ? event.detail.view : null;
if (view === 'posts') {
maybeAutoOpenPending('view');
} else {
cancelPendingAutoOpen(false);
}
});
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible' && pendingAutoOpenEnabled) {
if (pendingAutoOpenTimerId) {
clearTimeout(pendingAutoOpenTimerId);
pendingAutoOpenTimerId = null;
}
pendingAutoOpenTriggered = false;
maybeAutoOpenPending('visibility', PENDING_AUTO_OPEN_DELAY_MS);
}
});
// Initialize
async function bootstrapApp() {
const authenticated = await ensureAuthenticated();