Files
PostTracker/web/daily-bookmarks.js
2025-12-21 14:21:55 +01:00

1505 lines
47 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

(function () {
let active = false;
let initialized = false;
const API_URL = window.API_URL || 'https://fb.srv.medeba-media.de/api';
const BULK_COUNT_STORAGE_KEY = 'dailyBookmarkBulkCount';
const FILTER_STORAGE_KEY = 'dailyBookmarkFilters';
const SORT_STORAGE_KEY = 'dailyBookmarkSort';
const AUTO_OPEN_STORAGE_KEY = 'dailyBookmarkAutoOpen';
const DEFAULT_BULK_COUNT = 5;
const DEFAULT_SORT = { column: 'last_completed_at', direction: 'desc' };
const AUTO_OPEN_DELAY_MS = 1500;
const LOGIN_PAGE = 'login.html';
function handleUnauthorized(response) {
if (response && response.status === 401) {
if (typeof redirectToLogin === 'function') {
redirectToLogin();
} else {
window.location.href = LOGIN_PAGE;
}
return true;
}
return false;
}
const state = {
dayKey: formatDayKey(new Date()),
items: [],
loading: false,
saving: false,
processingBatch: false,
error: '',
bulkCount: loadBulkCount(),
filters: loadFilters(),
sort: loadSort(),
importing: false,
autoOpenEnabled: loadAutoOpenEnabled(),
autoOpenTriggered: false,
autoOpenTimerId: null,
autoOpenCountdownIntervalId: null
};
let editingId = null;
const dailyDayLabel = document.getElementById('dailyDayLabel');
const dailyDaySubLabel = document.getElementById('dailyDaySubLabel');
const dailyPrevDayBtn = document.getElementById('dailyPrevDayBtn');
const dailyNextDayBtn = document.getElementById('dailyNextDayBtn');
const dailyTodayBtn = document.getElementById('dailyTodayBtn');
const dailyRefreshBtn = document.getElementById('dailyRefreshBtn');
const dailyHeroStats = document.getElementById('dailyHeroStats');
const dailyListSummary = document.getElementById('dailyListSummary');
const dailyListStatus = document.getElementById('dailyListStatus');
const dailyTableBody = document.getElementById('dailyTableBody');
const dailyBulkCountSelect = document.getElementById('dailyBulkCountSelect');
const dailyBulkOpenBtn = document.getElementById('dailyBulkOpenBtn');
const dailyAutoOpenToggle = document.getElementById('dailyAutoOpenToggle');
const dailyAutoOpenOverlay = document.getElementById('dailyAutoOpenOverlay');
const dailyAutoOpenOverlayPanel = document.getElementById('dailyAutoOpenOverlayPanel');
const dailyAutoOpenCountdown = document.getElementById('dailyAutoOpenCountdown');
const dailyOpenCreateBtn = document.getElementById('dailyOpenCreateBtn');
const modal = document.getElementById('dailyBookmarkModal');
const dailyModalCloseBtn = document.getElementById('dailyModalCloseBtn');
const dailyModalBackdrop = modal ? modal.querySelector('.modal__backdrop') : null;
const formEl = document.getElementById('dailyBookmarkForm');
const dailyTitleInput = document.getElementById('dailyTitleInput');
const dailyUrlInput = document.getElementById('dailyUrlInput');
const dailyNotesInput = document.getElementById('dailyNotesInput');
const dailyResetBtn = document.getElementById('dailyResetBtn');
const dailySubmitBtn = document.getElementById('dailySubmitBtn');
const dailyPreviewLink = document.getElementById('dailyPreviewLink');
const dailyFormStatus = document.getElementById('dailyFormStatus');
const dailyFormModeLabel = document.getElementById('dailyFormModeLabel');
const dailyUrlSuggestionBox = document.getElementById('dailyUrlSuggestionBox');
const dailyMarkerInput = document.getElementById('dailyMarkerInput');
const dailyActiveInput = document.getElementById('dailyActiveInput');
const markerFilterSelect = document.getElementById('dailyMarkerFilter');
const urlFilterInput = document.getElementById('dailyUrlFilter');
const dailyResetViewBtn = document.getElementById('dailyResetViewBtn');
const sortButtons = Array.from(document.querySelectorAll('[data-sort-key]'));
const dailyOpenImportBtn = document.getElementById('dailyOpenImportBtn');
const dailyImportModal = document.getElementById('dailyImportModal');
const dailyImportCloseBtn = document.getElementById('dailyImportCloseBtn');
const dailyImportBackdrop = dailyImportModal ? dailyImportModal.querySelector('.modal__backdrop') : null;
const dailyImportForm = document.getElementById('dailyImportForm');
const dailyImportInput = document.getElementById('dailyImportInput');
const dailyImportMarkerInput = document.getElementById('dailyImportMarkerInput');
const dailyImportResetBtn = document.getElementById('dailyImportResetBtn');
const dailyImportSubmitBtn = document.getElementById('dailyImportSubmitBtn');
const dailyImportStatus = document.getElementById('dailyImportStatus');
const PLACEHOLDER_PATTERN = /\{\{\s*([^}]+)\s*\}\}/gi;
function ensureStyles(enabled) {
const link = document.getElementById('dailyBookmarksCss');
if (link) {
link.disabled = !enabled;
}
}
function formatDayKey(date) {
const d = date instanceof Date ? date : new Date();
const year = d.getFullYear();
const month = String(d.getMonth() + 1).padStart(2, '0');
const day = String(d.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}
function parseDayKey(dayKey) {
if (typeof dayKey !== 'string') {
return new Date();
}
const parts = dayKey.split('-').map((part) => parseInt(part, 10));
if (parts.length !== 3 || parts.some(Number.isNaN)) {
return new Date();
}
return new Date(parts[0], parts[1] - 1, parts[2]);
}
function addDays(date, delta) {
const base = date instanceof Date ? date : new Date();
const next = new Date(base);
next.setDate(base.getDate() + delta);
return next;
}
function formatDayLabel(dayKey) {
const date = parseDayKey(dayKey);
return date.toLocaleDateString('de-DE', {
weekday: 'long',
day: '2-digit',
month: 'long',
year: 'numeric'
});
}
function formatRelativeDay(dayKey) {
const target = parseDayKey(dayKey);
const today = new Date();
const todayKey = formatDayKey(today);
const diff = Math.round((target.setHours(0, 0, 0, 0) - new Date(todayKey).setHours(0, 0, 0, 0)) / 86400000);
if (diff === 0) return 'Heute';
if (diff === 1) return 'Morgen';
if (diff === -1) return 'Gestern';
if (diff > 1) return `In ${diff} Tagen`;
return `${Math.abs(diff)} Tage her`;
}
function loadBulkCount() {
try {
const stored = localStorage.getItem(BULK_COUNT_STORAGE_KEY);
const value = parseInt(stored, 10);
if (!Number.isNaN(value) && [1, 5, 10, 15, 20].includes(value)) {
return value;
}
} catch (error) {
// ignore
}
return DEFAULT_BULK_COUNT;
}
function persistBulkCount(value) {
try {
localStorage.setItem(BULK_COUNT_STORAGE_KEY, String(value));
} catch (error) {
// ignore
}
}
function loadFilters() {
try {
const raw = localStorage.getItem(FILTER_STORAGE_KEY);
if (raw) {
const parsed = JSON.parse(raw);
return {
marker: typeof parsed.marker === 'string' ? parsed.marker : '',
url: typeof parsed.url === 'string' ? parsed.url : ''
};
}
} catch (error) {
// ignore
}
return { marker: '', url: '' };
}
function persistFilters(filters) {
try {
localStorage.setItem(FILTER_STORAGE_KEY, JSON.stringify(filters || {}));
} catch (error) {
// ignore
}
}
function loadSort() {
try {
const raw = localStorage.getItem(SORT_STORAGE_KEY);
if (raw) {
const parsed = JSON.parse(raw);
const allowedColumns = ['url_template', 'marker', 'created_at', 'updated_at', 'last_completed_at'];
const allowedDirections = ['asc', 'desc'];
if (
parsed &&
allowedColumns.includes(parsed.column) &&
allowedDirections.includes(parsed.direction)
) {
return parsed;
}
}
} catch (error) {
// ignore
}
return { ...DEFAULT_SORT };
}
function persistSort(sort) {
try {
localStorage.setItem(SORT_STORAGE_KEY, JSON.stringify(sort || DEFAULT_SORT));
} catch (error) {
// ignore
}
}
function loadAutoOpenEnabled() {
try {
return localStorage.getItem(AUTO_OPEN_STORAGE_KEY) === '1';
} catch (error) {
return false;
}
}
function persistAutoOpenEnabled(enabled) {
try {
localStorage.setItem(AUTO_OPEN_STORAGE_KEY, enabled ? '1' : '0');
} catch (error) {
// ignore
}
}
function normalizeItem(item) {
if (!item || typeof item !== 'object') {
return null;
}
return {
...item,
is_active: Number(item.is_active ?? 1) !== 0
};
}
function resolveTemplate(template, dayKey) {
if (typeof template !== 'string') {
return '';
}
const baseDate = parseDayKey(dayKey);
return template.replace(PLACEHOLDER_PATTERN, (_match, contentRaw) => {
const content = (contentRaw || '').trim();
if (!content) {
return '';
}
const counterMatch = content.match(/^counter:\s*([+-]?\d+)([+-]\d+)?$/i);
if (counterMatch) {
const base = parseInt(counterMatch[1], 10);
const offset = counterMatch[2] ? parseInt(counterMatch[2], 10) || 0 : 0;
if (Number.isNaN(base)) {
return '';
}
const date = addDays(baseDate, offset);
return String(base + date.getDate());
}
const placeholderMatch = content.match(/^(date|day|dd|mm|month|yyyy|yy)([+-]\d+)?$/i);
if (!placeholderMatch) {
return content;
}
const token = String(placeholderMatch[1] || '').toLowerCase();
const offset = placeholderMatch[2] ? parseInt(placeholderMatch[2], 10) || 0 : 0;
const date = addDays(baseDate, offset);
switch (token) {
case 'date':
return formatDayKey(date);
case 'day':
return String(date.getDate());
case 'dd':
return String(date.getDate()).padStart(2, '0');
case 'month':
case 'mm':
return String(date.getMonth() + 1).padStart(2, '0');
case 'yyyy':
return String(date.getFullYear());
case 'yy':
return String(date.getFullYear()).slice(-2);
default:
return token;
}
});
}
function isDelimiter(char) {
return !char || /[\\/_\\-\\.\\?#&=]/.test(char);
}
function buildUrlSuggestions(urlValue) {
const suggestions = [];
if (!urlValue || typeof urlValue !== 'string') {
return suggestions;
}
const raw = urlValue.trim();
if (!raw) {
return suggestions;
}
const today = parseDayKey(state.dayKey);
const day = today.getDate();
const yearFull = String(today.getFullYear());
const yearShort = yearFull.slice(-2);
const monthNumber = today.getMonth() + 1;
const tokens = [];
const addToken = (text, placeholder) => {
if (!text) return;
tokens.push({ text, placeholder });
};
const dayText = String(day);
addToken(dayText, '{{day}}');
const dayPadded = dayText.padStart(2, '0');
if (dayPadded !== dayText) {
addToken(dayPadded, '{{dd}}');
}
const monthText = String(monthNumber);
addToken(monthText, '{{mm}}');
const monthPadded = monthText.padStart(2, '0');
if (monthPadded !== monthText) {
addToken(monthPadded, '{{mm}}');
}
addToken(yearFull, '{{yyyy}}');
addToken(yearShort, '{{yy}}');
const escapeRegex = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
function findOccurrences(text, tokenText) {
const pattern = new RegExp(`(^|[^0-9])(${escapeRegex(tokenText)})(?=$|[^0-9])`, 'g');
const occurrences = [];
let match;
while ((match = pattern.exec(text)) !== null) {
const prefixLength = (match[1] || '').length;
const startIndex = match.index + prefixLength;
occurrences.push({ index: startIndex, length: tokenText.length });
}
return occurrences;
}
for (const token of tokens) {
const occ = findOccurrences(raw, token.text);
if (!occ.length) {
continue;
}
const candidate = occ[occ.length - 1]; // prefer last occurrence (z.B. am URL-Ende)
const replaced = `${raw.slice(0, candidate.index)}${token.placeholder}${raw.slice(candidate.index + candidate.length)}`;
const label = `Ersetze „${token.text}“ durch ${token.placeholder}`;
if (!suggestions.some((s) => s.label === label && s.value === replaced)) {
suggestions.push({
label,
value: replaced
});
}
}
return suggestions;
}
function renderUrlSuggestions() {
if (!dailyUrlSuggestionBox) {
return;
}
const suggestions = buildUrlSuggestions(dailyUrlInput ? dailyUrlInput.value : '');
dailyUrlSuggestionBox.innerHTML = '';
if (!suggestions.length) {
dailyUrlSuggestionBox.hidden = true;
return;
}
const text = document.createElement('span');
text.className = 'suggestion-box__text';
text.textContent = 'Mögliche Platzhalter:';
dailyUrlSuggestionBox.appendChild(text);
const applySuggestion = (value) => {
if (!dailyUrlInput) {
return;
}
dailyUrlInput.value = value;
updatePreviewLink();
renderUrlSuggestions();
const end = dailyUrlInput.value.length;
dailyUrlInput.focus();
try {
dailyUrlInput.setSelectionRange(end, end);
} catch (error) {
// ignore if not supported
}
};
suggestions.forEach((sugg) => {
const item = document.createElement('div');
item.className = 'suggestion-box__item';
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'suggestion-btn';
btn.textContent = sugg.label;
btn.addEventListener('click', () => {
applySuggestion(sugg.value);
});
item.appendChild(btn);
const preview = document.createElement('a');
preview.className = 'suggestion-preview';
preview.href = resolveTemplate(sugg.value, state.dayKey) || sugg.value;
preview.target = '_blank';
preview.rel = 'noopener';
preview.title = resolveTemplate(sugg.value, state.dayKey) || sugg.value;
preview.textContent = resolveTemplate(sugg.value, state.dayKey) || sugg.value;
preview.addEventListener('click', (event) => {
event.preventDefault();
applySuggestion(sugg.value);
});
item.appendChild(preview);
dailyUrlSuggestionBox.appendChild(item);
});
dailyUrlSuggestionBox.hidden = false;
}
async function apiFetch(url, options = {}) {
const response = await fetch(url, {
...options,
credentials: 'include',
headers: {
'Content-Type': 'application/json',
...(options.headers || {})
}
});
if (handleUnauthorized(response)) {
throw new Error('Nicht angemeldet');
}
if (!response.ok) {
const message = `Fehler: HTTP ${response.status}`;
throw new Error(message);
}
if (response.status === 204) {
return null;
}
const text = await response.text();
if (!text) {
return null;
}
try {
return JSON.parse(text);
} catch (error) {
return null;
}
}
function updateDayUI() {
if (dailyDayLabel) {
dailyDayLabel.textContent = formatDayLabel(state.dayKey);
}
if (dailyDaySubLabel) {
dailyDaySubLabel.textContent = formatRelativeDay(state.dayKey);
}
updatePreviewLink();
}
function setDayKey(dayKey) {
state.dayKey = formatDayKey(parseDayKey(dayKey));
if (!active) return;
updateDayUI();
loadDailyBookmarks();
}
function setFormStatus(message, isError = false) {
if (!dailyFormStatus) {
return;
}
dailyFormStatus.textContent = message || '';
dailyFormStatus.classList.toggle('form-status--error', !!isError);
}
function setListStatus(message, isError = false) {
if (!dailyListStatus) {
return;
}
dailyListStatus.textContent = message || '';
dailyListStatus.classList.toggle('list-status--error', !!isError);
}
function updatePreviewLink() {
if (!dailyPreviewLink || !dailyUrlInput) {
return;
}
const resolved = resolveTemplate(dailyUrlInput.value || '', state.dayKey);
dailyPreviewLink.textContent = resolved || '';
if (resolved) {
dailyPreviewLink.href = resolved;
dailyPreviewLink.target = '_blank';
dailyPreviewLink.rel = 'noopener';
} else {
dailyPreviewLink.removeAttribute('href');
}
renderUrlSuggestions();
}
function resetForm() {
editingId = null;
dailyFormModeLabel.textContent = 'Neues Bookmark';
dailySubmitBtn.textContent = 'Speichern';
formEl.reset();
if (dailyMarkerInput) {
dailyMarkerInput.value = '';
}
if (dailyActiveInput) {
dailyActiveInput.checked = true;
}
setFormStatus('');
updatePreviewLink();
renderUrlSuggestions();
}
function openModal(mode, bookmark) {
if (dailyImportModal && !dailyImportModal.hidden) {
closeImportModal();
}
if (mode === 'edit' && bookmark) {
editingId = bookmark.id;
dailyFormModeLabel.textContent = 'Bookmark bearbeiten';
dailySubmitBtn.textContent = 'Aktualisieren';
dailyTitleInput.value = bookmark.title || '';
dailyUrlInput.value = bookmark.url_template || '';
dailyNotesInput.value = bookmark.notes || '';
if (dailyMarkerInput) {
dailyMarkerInput.value = bookmark.marker || '';
}
if (dailyActiveInput) {
dailyActiveInput.checked = bookmark.is_active !== false;
}
setFormStatus('Bearbeite vorhandenes Bookmark');
} else {
resetForm();
}
if (modal) {
modal.hidden = false;
modal.focus();
}
updatePreviewLink();
if (mode === 'edit' && dailyTitleInput) {
dailyTitleInput.focus();
} else if (dailyUrlInput) {
dailyUrlInput.focus();
}
}
function closeModal() {
if (modal) {
modal.hidden = true;
}
resetForm();
}
function formatRelativeTimestamp(value) {
if (!value) return 'Noch nie erledigt';
const date = new Date(value);
if (Number.isNaN(date.getTime())) {
return 'Zeitpunkt unbekannt';
}
const diffMs = Date.now() - date.getTime();
const diffMin = Math.floor(diffMs / 60000);
if (diffMin < 1) return 'Gerade erledigt';
if (diffMin < 60) return `Vor ${diffMin} Min`;
const diffHours = Math.floor(diffMin / 60);
if (diffHours < 24) return `Vor ${diffHours} Std`;
const diffDays = Math.floor(diffHours / 24);
return `Vor ${diffDays} Tagen`;
}
function formatDateStamp(value) {
if (!value) return '—';
const date = new Date(value);
if (Number.isNaN(date.getTime())) {
return '—';
}
return date.toLocaleString('de-DE', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
}
function normalizeMarkerValue(value) {
return (value || '').trim();
}
function getFilteredItems() {
const dailyMarkerFilter = state.filters.marker || '';
const dailyUrlFilter = (state.filters.url || '').toLowerCase();
return state.items.filter((item) => {
const currentMarker = normalizeMarkerValue(item.marker).toLowerCase();
if (dailyMarkerFilter === '__none') {
if (currentMarker) {
return false;
}
} else if (dailyMarkerFilter) {
if (currentMarker !== dailyMarkerFilter.toLowerCase()) {
return false;
}
}
if (dailyUrlFilter) {
const urlValue = (item.resolved_url || item.url_template || '').toLowerCase();
if (!urlValue.includes(dailyUrlFilter)) {
return false;
}
}
return true;
});
}
function sortItems(items) {
const sorted = [...items];
const activeSort = state.sort || DEFAULT_SORT;
const direction = activeSort.direction === 'asc' ? 1 : -1;
const safeTime = (value) => {
const time = value ? new Date(value).getTime() : 0;
return Number.isNaN(time) ? 0 : time;
};
sorted.sort((a, b) => {
switch (activeSort.column) {
case 'url_template':
return a.url_template.localeCompare(b.url_template, 'de', { sensitivity: 'base' }) * direction;
case 'marker':
return normalizeMarkerValue(a.marker).localeCompare(normalizeMarkerValue(b.marker), 'de', { sensitivity: 'base' }) * direction;
case 'created_at':
return (safeTime(a.created_at) - safeTime(b.created_at)) * direction;
case 'updated_at':
return (safeTime(a.updated_at) - safeTime(b.updated_at)) * direction;
case 'last_completed_at':
default:
return (safeTime(a.last_completed_at) - safeTime(b.last_completed_at)) * direction;
}
});
return sorted;
}
function getVisibleItems() {
return sortItems(getFilteredItems());
}
function updateSortIndicators() {
if (!sortButtons || !sortButtons.length) {
return;
}
const activeSort = state.sort || DEFAULT_SORT;
sortButtons.forEach((btn) => {
const key = btn.dataset.sortKey;
const isActive = key === activeSort.column;
btn.classList.toggle('is-active', isActive);
btn.setAttribute('data-sort-direction', isActive ? (activeSort.direction === 'asc' ? '↑' : '↓') : '↕');
});
}
function renderMarkerFilterOptions() {
if (!markerFilterSelect) {
return;
}
const activeValue = state.filters.marker || '';
const markers = Array.from(
new Set(
state.items
.map((item) => normalizeMarkerValue(item.marker))
.filter(Boolean)
)
).sort((a, b) => a.localeCompare(b, 'de', { sensitivity: 'base' }));
markerFilterSelect.innerHTML = '';
const addOption = (value, label) => {
const opt = document.createElement('option');
opt.value = value;
opt.textContent = label;
markerFilterSelect.appendChild(opt);
};
addOption('', 'Alle Marker');
addOption('__none', 'Ohne Marker');
markers.forEach((marker) => addOption(marker, marker));
if (activeValue && activeValue !== '__none' && !markers.includes(activeValue)) {
addOption(activeValue, `${activeValue} (kein Treffer)`);
}
const nextValue = activeValue || '';
markerFilterSelect.value = nextValue;
state.filters.marker = nextValue;
persistFilters(state.filters);
}
function renderTable() {
if (!dailyTableBody) {
return;
}
dailyTableBody.innerHTML = '';
updateSortIndicators();
if (state.loading) {
appendSingleRow('Lade Bookmarks...', 'loading-state');
return;
}
if (state.error) {
appendSingleRow(state.error, 'error-state');
return;
}
if (!state.items.length) {
appendSingleRow('Keine Bookmarks vorhanden. Lege dein erstes Bookmark an.', 'empty-state');
return;
}
const visibleItems = getVisibleItems();
if (!visibleItems.length) {
appendSingleRow('Keine Bookmarks für diesen Filter.', 'empty-state');
return;
}
visibleItems.forEach((item) => {
const tr = document.createElement('tr');
const isActive = item.is_active !== false;
if (isActive) {
tr.classList.add(item.completed_for_day ? 'is-done' : 'is-open');
} else {
tr.classList.add('is-inactive');
}
const urlTd = document.createElement('td');
urlTd.className = 'url-cell';
const link = document.createElement('a');
const resolvedUrl = item.resolved_url || item.url_template;
link.href = resolvedUrl;
link.target = '_blank';
link.rel = 'noopener';
link.title = resolvedUrl;
link.textContent = resolvedUrl;
urlTd.appendChild(link);
tr.appendChild(urlTd);
const markerTd = document.createElement('td');
markerTd.className = 'marker-cell';
markerTd.textContent = '';
if (normalizeMarkerValue(item.marker)) {
const markerChip = document.createElement('span');
markerChip.className = 'chip chip--marker';
markerChip.textContent = item.marker;
markerTd.appendChild(markerChip);
} else {
const placeholder = document.createElement('span');
placeholder.textContent = '';
placeholder.classList.add('muted');
markerTd.appendChild(placeholder);
}
if (!isActive) {
const inactiveChip = document.createElement('span');
inactiveChip.className = 'chip chip--inactive';
inactiveChip.textContent = 'Deaktiviert';
markerTd.appendChild(inactiveChip);
}
tr.appendChild(markerTd);
const createdTd = document.createElement('td');
createdTd.textContent = formatDateStamp(item.created_at);
tr.appendChild(createdTd);
const timeTd = document.createElement('td');
timeTd.textContent = formatRelativeTimestamp(item.last_completed_at);
tr.appendChild(timeTd);
const actionsTd = document.createElement('td');
actionsTd.className = 'table-actions';
const openBtn = document.createElement('button');
openBtn.className = 'ghost-btn';
openBtn.type = 'button';
openBtn.textContent = '🔗';
openBtn.title = isActive ? 'Öffnen' : 'Deaktiviert';
openBtn.disabled = !isActive;
openBtn.addEventListener('click', () => {
const target = item.resolved_url || item.url_template;
if (!isActive) return;
if (target) {
window.open(target, '_blank', 'noopener');
}
});
actionsTd.appendChild(openBtn);
const toggleBtn = document.createElement('button');
toggleBtn.className = 'ghost-btn';
toggleBtn.type = 'button';
toggleBtn.textContent = item.completed_for_day ? '↩️' : '✅';
toggleBtn.title = isActive
? (item.completed_for_day ? 'Zurücksetzen' : 'Heute erledigt')
: 'Deaktiviert';
toggleBtn.disabled = !isActive;
toggleBtn.addEventListener('click', () => {
if (item.completed_for_day) {
undoDailyBookmark(item.id);
} else {
completeDailyBookmark(item.id);
}
});
actionsTd.appendChild(toggleBtn);
const editBtn = document.createElement('button');
editBtn.className = 'ghost-btn';
editBtn.type = 'button';
editBtn.textContent = '✏️';
editBtn.title = 'Bearbeiten';
editBtn.addEventListener('click', () => openModal('edit', item));
actionsTd.appendChild(editBtn);
const deleteBtn = document.createElement('button');
deleteBtn.className = 'ghost-btn danger';
deleteBtn.type = 'button';
deleteBtn.textContent = '🗑️';
deleteBtn.title = 'Löschen';
deleteBtn.addEventListener('click', () => {
if (window.confirm('Bookmark wirklich löschen?')) {
deleteDailyBookmark(item.id);
}
});
actionsTd.appendChild(deleteBtn);
tr.appendChild(actionsTd);
dailyTableBody.appendChild(tr);
});
}
function appendSingleRow(text, className) {
const tr = document.createElement('tr');
const td = document.createElement('td');
td.colSpan = 5;
td.className = className;
td.textContent = text;
tr.appendChild(td);
dailyTableBody.appendChild(tr);
}
function updateHeroStats() {
const activeItems = state.items.filter((item) => item.is_active !== false);
const totalActive = activeItems.length;
const inactiveCount = state.items.length - totalActive;
const done = activeItems.filter((item) => item.completed_for_day).length;
const visibleItems = getFilteredItems().filter((item) => item.is_active !== false);
const visibleDone = visibleItems.filter((item) => item.completed_for_day).length;
const filterSuffix = visibleItems.length !== totalActive ? ` · Gefiltert: ${visibleDone}/${visibleItems.length}` : '';
const inactiveSuffix = inactiveCount ? ` · ${inactiveCount} deaktiviert` : '';
const baseText = totalActive ? `${done}/${totalActive} erledigt${filterSuffix}` : 'Keine aktiven Bookmarks';
const text = `${baseText}${inactiveSuffix}`;
const filterParts = [];
if (state.filters.marker) {
filterParts.push(state.filters.marker === '__none' ? 'ohne Marker' : `Marker: ${state.filters.marker}`);
}
if (state.filters.url) {
filterParts.push(`URL enthält „${state.filters.url}`);
}
const filterText = filterParts.length ? ` · Filter: ${filterParts.join(' · ')}` : '';
if (dailyHeroStats) {
dailyHeroStats.textContent = `${text}${filterText}`;
}
if (dailyListSummary) {
dailyListSummary.textContent = `${text} Tag ${state.dayKey}${filterText}`;
}
}
function clearAutoOpenCountdown() {
if (state.autoOpenCountdownIntervalId) {
clearInterval(state.autoOpenCountdownIntervalId);
state.autoOpenCountdownIntervalId = null;
}
}
function updateAutoOpenCountdownLabel(remainingMs) {
if (!dailyAutoOpenCountdown) return;
const safeMs = Math.max(0, remainingMs);
const seconds = safeMs / 1000;
const formatted = seconds >= 10 ? seconds.toFixed(0) : seconds.toFixed(1);
dailyAutoOpenCountdown.textContent = formatted;
}
function hideAutoOpenOverlay() {
clearAutoOpenCountdown();
if (dailyAutoOpenOverlay) {
dailyAutoOpenOverlay.classList.remove('visible');
dailyAutoOpenOverlay.hidden = true;
}
}
function showAutoOpenOverlay(delayMs) {
if (!dailyAutoOpenOverlay) return;
const duration = Math.max(0, delayMs);
hideAutoOpenOverlay();
dailyAutoOpenOverlay.hidden = false;
requestAnimationFrame(() => dailyAutoOpenOverlay.classList.add('visible'));
updateAutoOpenCountdownLabel(duration);
const start = Date.now();
state.autoOpenCountdownIntervalId = setInterval(() => {
const remaining = Math.max(0, duration - (Date.now() - start));
updateAutoOpenCountdownLabel(remaining);
if (remaining <= 0) {
clearAutoOpenCountdown();
}
}, 100);
}
function cancelAutoOpen(showMessage = false) {
if (state.autoOpenTimerId) {
clearTimeout(state.autoOpenTimerId);
state.autoOpenTimerId = null;
}
state.autoOpenTriggered = false;
hideAutoOpenOverlay();
if (showMessage) {
setListStatus('Automatisches Öffnen abgebrochen.', false);
}
}
function maybeAutoOpen(reason = '', delayMs = AUTO_OPEN_DELAY_MS) {
if (!active) {
hideAutoOpenOverlay();
return;
}
if (!state.autoOpenEnabled) {
hideAutoOpenOverlay();
return;
}
if (state.processingBatch) return;
if (state.autoOpenTriggered) return;
const undone = getVisibleItems().filter(
(item) => item.is_active !== false && !item.completed_for_day
);
if (!undone.length) {
hideAutoOpenOverlay();
return;
}
if (state.autoOpenTimerId) {
clearTimeout(state.autoOpenTimerId);
state.autoOpenTimerId = null;
}
hideAutoOpenOverlay();
state.autoOpenTriggered = true;
const delay = typeof delayMs === 'number' ? Math.max(0, delayMs) : AUTO_OPEN_DELAY_MS;
if (delay === 0) {
if (state.autoOpenEnabled) {
openBatch({ auto: true });
} else {
state.autoOpenTriggered = false;
}
return;
}
showAutoOpenOverlay(delay);
state.autoOpenTimerId = setTimeout(() => {
state.autoOpenTimerId = null;
hideAutoOpenOverlay();
if (state.autoOpenEnabled) {
openBatch({ auto: true });
} else {
state.autoOpenTriggered = false;
}
}, delay);
}
async function loadDailyBookmarks() {
if (!active) return;
state.loading = true;
if (state.autoOpenTimerId) {
clearTimeout(state.autoOpenTimerId);
state.autoOpenTimerId = null;
}
hideAutoOpenOverlay();
state.autoOpenTriggered = false;
state.error = '';
setListStatus('');
renderTable();
try {
const data = await apiFetch(`${API_URL}/daily-bookmarks?day=${encodeURIComponent(state.dayKey)}`);
const rows = Array.isArray(data) ? data : [];
state.items = rows.map((item) => normalizeItem(item)).filter(Boolean);
state.loading = false;
updateHeroStats();
renderMarkerFilterOptions();
renderTable();
maybeAutoOpen('load');
} catch (error) {
state.loading = false;
state.error = 'Konnte Bookmarks nicht laden.';
setListStatus(state.error, true);
renderTable();
}
}
async function completeDailyBookmark(id) {
if (!id) return;
const target = state.items.find((item) => item.id === id);
if (target && target.is_active === false) {
setListStatus('Bookmark ist deaktiviert.', true);
return;
}
state.error = '';
try {
const updated = await apiFetch(`${API_URL}/daily-bookmarks/${encodeURIComponent(id)}/check`, {
method: 'POST',
body: JSON.stringify({ day: state.dayKey })
});
const normalized = normalizeItem(updated);
state.items = state.items.map((item) => (item.id === id ? normalized || item : item));
updateHeroStats();
renderTable();
} catch (error) {
state.error = 'Konnte nicht abhaken.';
setListStatus(state.error, true);
renderTable();
}
}
async function undoDailyBookmark(id) {
if (!id) return;
const target = state.items.find((item) => item.id === id);
if (target && target.is_active === false) {
setListStatus('Bookmark ist deaktiviert.', true);
return;
}
state.error = '';
try {
const updated = await apiFetch(`${API_URL}/daily-bookmarks/${encodeURIComponent(id)}/check?day=${encodeURIComponent(state.dayKey)}`, {
method: 'DELETE'
});
const normalized = normalizeItem(updated);
state.items = state.items.map((item) => (item.id === id ? normalized || item : item));
updateHeroStats();
renderTable();
} catch (error) {
state.error = 'Konnte nicht zurücksetzen.';
setListStatus(state.error, true);
renderTable();
}
}
async function deleteDailyBookmark(id) {
if (!id) return;
state.error = '';
try {
await apiFetch(`${API_URL}/daily-bookmarks/${encodeURIComponent(id)}`, {
method: 'DELETE'
});
state.items = state.items.filter((item) => item.id !== id);
if (editingId === id) {
resetForm();
}
updateHeroStats();
renderMarkerFilterOptions();
renderTable();
} catch (error) {
state.error = 'Konnte Bookmark nicht löschen.';
setListStatus(state.error, true);
renderTable();
}
}
async function submitForm(event) {
event.preventDefault();
if (state.saving) return;
const title = dailyTitleInput.value.trim();
const url = dailyUrlInput.value.trim();
const notes = dailyNotesInput.value.trim();
const marker = dailyMarkerInput ? dailyMarkerInput.value.trim() : '';
if (!url) {
setFormStatus('URL-Template ist Pflicht.', true);
dailyUrlInput.focus();
return;
}
state.saving = true;
dailySubmitBtn.disabled = true;
setFormStatus('');
const payload = {
title,
url_template: url,
notes,
marker,
is_active: dailyActiveInput ? dailyActiveInput.checked : true,
day: state.dayKey
};
const targetUrl = editingId
? `${API_URL}/daily-bookmarks/${encodeURIComponent(editingId)}`
: `${API_URL}/daily-bookmarks`;
const method = editingId ? 'PUT' : 'POST';
try {
const saved = await apiFetch(targetUrl, {
method,
body: JSON.stringify(payload)
});
const normalized = normalizeItem(saved);
if (editingId) {
state.items = state.items.map((item) => (item.id === editingId ? normalized || item : item));
} else if (normalized) {
state.items = [normalized, ...state.items];
}
updateHeroStats();
renderMarkerFilterOptions();
renderTable();
closeModal();
} catch (error) {
setFormStatus('Speichern fehlgeschlagen.', true);
} finally {
state.saving = false;
dailySubmitBtn.disabled = false;
}
}
async function openBatch({ auto = false } = {}) {
if (state.processingBatch) return;
if (!auto) {
cancelAutoOpen(false);
}
const undone = getVisibleItems().filter(
(item) => item.is_active !== false && !item.completed_for_day
);
if (!undone.length) {
if (!auto) {
setListStatus('Keine offenen Bookmarks für den gewählten Tag.', true);
}
return;
}
const count = state.bulkCount || DEFAULT_BULK_COUNT;
const selection = undone.slice(0, count);
state.processingBatch = true;
if (dailyBulkOpenBtn) {
dailyBulkOpenBtn.disabled = true;
}
if (!auto) {
setListStatus('');
} else {
setListStatus(`Öffne automatisch ${selection.length} Links...`, false);
}
for (const item of selection) {
const target = item.resolved_url || item.url_template;
if (target) {
window.open(target, '_blank', 'noopener');
}
try {
const updated = await apiFetch(`${API_URL}/daily-bookmarks/${encodeURIComponent(item.id)}/check`, {
method: 'POST',
body: JSON.stringify({ day: state.dayKey })
});
const normalized = normalizeItem(updated);
state.items = state.items.map((entry) => (entry.id === item.id ? normalized || entry : entry));
} catch (error) {
setListStatus('Einige Bookmarks konnten nicht abgehakt werden.', true);
}
}
state.processingBatch = false;
if (dailyBulkOpenBtn) {
dailyBulkOpenBtn.disabled = false;
}
if (auto) {
setListStatus('');
}
updateHeroStats();
renderTable();
}
function setImportStatus(message, isError = false) {
if (!dailyImportStatus) {
return;
}
dailyImportStatus.textContent = message || '';
dailyImportStatus.classList.toggle('form-status--error', !!isError);
}
function resetImportForm() {
if (dailyImportForm) {
dailyImportForm.reset();
}
const suggestedMarker = state.filters.marker && state.filters.marker !== '__none' ? state.filters.marker : '';
if (dailyImportMarkerInput) {
dailyImportMarkerInput.value = suggestedMarker;
}
setImportStatus('');
}
function openImportModal() {
if (modal && !modal.hidden) {
closeModal();
}
resetImportForm();
if (dailyImportModal) {
dailyImportModal.hidden = false;
dailyImportModal.focus();
}
if (dailyImportInput) {
dailyImportInput.focus();
}
}
function closeImportModal() {
if (dailyImportModal) {
dailyImportModal.hidden = true;
}
resetImportForm();
}
async function submitImportForm(event) {
event.preventDefault();
if (state.importing) return;
const rawText = dailyImportInput ? dailyImportInput.value.trim() : '';
const marker = dailyImportMarkerInput ? dailyImportMarkerInput.value.trim() : '';
if (!rawText) {
setImportStatus('Bitte füge mindestens eine URL ein.', true);
if (dailyImportInput) {
dailyImportInput.focus();
}
return;
}
const urls = rawText
.split(/\r?\n/)
.map((line) => line.trim())
.filter(Boolean);
if (!urls.length) {
setImportStatus('Keine gültigen Zeilen gefunden.', true);
return;
}
state.importing = true;
if (dailyImportSubmitBtn) {
dailyImportSubmitBtn.disabled = true;
}
setImportStatus('Import läuft...');
try {
const result = await apiFetch(`${API_URL}/daily-bookmarks/import`, {
method: 'POST',
body: JSON.stringify({ urls, marker, day: state.dayKey })
});
const importedItemsRaw = Array.isArray(result && result.items) ? result.items : [];
const importedItems = importedItemsRaw.map((item) => normalizeItem(item)).filter(Boolean);
const importedIds = new Set(importedItems.map((entry) => entry.id));
const remaining = state.items.filter((item) => !importedIds.has(item.id));
state.items = [...importedItems, ...remaining];
const summaryParts = [];
const createdCount = result && typeof result.created === 'number' ? result.created : importedItems.length;
summaryParts.push(`${createdCount} neu`);
if (result && result.skipped_existing) summaryParts.push(`${result.skipped_existing} bereits vorhanden`);
if (result && result.skipped_invalid) summaryParts.push(`${result.skipped_invalid} ungültig`);
if (result && result.skipped_duplicates) summaryParts.push(`${result.skipped_duplicates} Duplikate`);
setListStatus(`Import fertig: ${summaryParts.join(' · ')}`);
updateHeroStats();
renderMarkerFilterOptions();
renderTable();
closeImportModal();
} catch (error) {
setImportStatus(error && error.message ? error.message : 'Import fehlgeschlagen.', true);
} finally {
state.importing = false;
if (dailyImportSubmitBtn) {
dailyImportSubmitBtn.disabled = false;
}
}
}
function setupEvents() {
if (dailyPrevDayBtn) {
dailyPrevDayBtn.addEventListener('click', () => setDayKey(formatDayKey(addDays(parseDayKey(state.dayKey), -1))));
}
if (dailyNextDayBtn) {
dailyNextDayBtn.addEventListener('click', () => setDayKey(formatDayKey(addDays(parseDayKey(state.dayKey), 1))));
}
if (dailyTodayBtn) {
dailyTodayBtn.addEventListener('click', () => setDayKey(formatDayKey(new Date())));
}
if (dailyRefreshBtn) {
dailyRefreshBtn.addEventListener('click', () => loadDailyBookmarks());
}
if (dailyOpenCreateBtn) {
dailyOpenCreateBtn.addEventListener('click', () => openModal('create'));
}
if (dailyModalCloseBtn) {
dailyModalCloseBtn.addEventListener('click', closeModal);
}
if (modal) {
modal.addEventListener('click', (event) => {
if (event.target === modal) {
closeModal();
}
});
}
if (dailyModalBackdrop) {
dailyModalBackdrop.addEventListener('click', closeModal);
}
document.addEventListener('keydown', (event) => {
if (!active) return;
if (event.key === 'Escape') {
if (modal && !modal.hidden) {
closeModal();
}
if (dailyImportModal && !dailyImportModal.hidden) {
closeImportModal();
}
}
});
if (dailyUrlInput) {
dailyUrlInput.addEventListener('input', updatePreviewLink);
dailyUrlInput.addEventListener('blur', renderUrlSuggestions);
}
if (formEl) {
formEl.addEventListener('submit', submitForm);
}
if (dailyResetBtn) {
dailyResetBtn.addEventListener('click', resetForm);
}
if (dailyBulkCountSelect) {
dailyBulkCountSelect.value = String(state.bulkCount);
dailyBulkCountSelect.addEventListener('change', () => {
const value = parseInt(dailyBulkCountSelect.value, 10);
if (!Number.isNaN(value)) {
state.bulkCount = value;
persistBulkCount(value);
}
});
}
if (dailyBulkOpenBtn) {
dailyBulkOpenBtn.addEventListener('click', () => openBatch());
}
if (dailyAutoOpenOverlayPanel) {
dailyAutoOpenOverlayPanel.addEventListener('click', () => cancelAutoOpen(true));
}
if (dailyAutoOpenToggle) {
dailyAutoOpenToggle.checked = !!state.autoOpenEnabled;
dailyAutoOpenToggle.addEventListener('change', () => {
state.autoOpenEnabled = dailyAutoOpenToggle.checked;
persistAutoOpenEnabled(state.autoOpenEnabled);
state.autoOpenTriggered = false;
if (!state.autoOpenEnabled && state.autoOpenTimerId) {
cancelAutoOpen(false);
}
if (state.autoOpenEnabled) {
maybeAutoOpen('toggle');
} else {
hideAutoOpenOverlay();
}
});
}
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible' && state.autoOpenEnabled) {
// Reset the guard so that returning to the tab can trigger the next batch.
if (state.autoOpenTimerId) {
clearTimeout(state.autoOpenTimerId);
state.autoOpenTimerId = null;
}
state.autoOpenTriggered = false;
maybeAutoOpen('visibility', AUTO_OPEN_DELAY_MS);
}
});
if (markerFilterSelect) {
markerFilterSelect.addEventListener('change', () => {
state.filters.marker = markerFilterSelect.value || '';
persistFilters(state.filters);
updateHeroStats();
renderTable();
});
}
if (urlFilterInput) {
urlFilterInput.value = state.filters.url || '';
urlFilterInput.addEventListener('input', () => {
state.filters.url = urlFilterInput.value.trim();
persistFilters(state.filters);
updateHeroStats();
renderTable();
});
}
if (dailyResetViewBtn) {
dailyResetViewBtn.addEventListener('click', () => {
state.filters = { marker: '', url: '' };
state.sort = { ...DEFAULT_SORT };
persistFilters(state.filters);
persistSort(state.sort);
renderMarkerFilterOptions();
if (urlFilterInput) {
urlFilterInput.value = '';
}
updateHeroStats();
renderTable();
});
}
if (sortButtons && sortButtons.length) {
sortButtons.forEach((btn) => {
btn.addEventListener('click', () => {
const key = btn.dataset.sortKey;
if (!key) return;
if (state.sort.column === key) {
state.sort.direction = state.sort.direction === 'asc' ? 'desc' : 'asc';
} else {
const defaultDirection = ['last_completed_at', 'updated_at', 'created_at'].includes(key) ? 'desc' : 'asc';
state.sort = { column: key, direction: defaultDirection };
}
persistSort(state.sort);
renderTable();
});
});
}
if (dailyOpenImportBtn) {
dailyOpenImportBtn.addEventListener('click', openImportModal);
}
if (dailyImportCloseBtn) {
dailyImportCloseBtn.addEventListener('click', closeImportModal);
}
if (dailyImportModal) {
dailyImportModal.addEventListener('click', (event) => {
if (event.target === dailyImportModal) {
closeImportModal();
}
});
}
if (dailyImportBackdrop) {
dailyImportBackdrop.addEventListener('click', closeImportModal);
}
if (dailyImportForm) {
dailyImportForm.addEventListener('submit', submitImportForm);
}
if (dailyImportResetBtn) {
dailyImportResetBtn.addEventListener('click', resetImportForm);
}
}
function init() {
if (initialized) return;
setupEvents();
initialized = true;
}
function cleanup() {
active = false;
cancelAutoOpen(false);
ensureStyles(false);
hideAutoOpenOverlay();
}
function activate() {
ensureStyles(true);
init();
active = true;
updateDayUI();
updatePreviewLink();
loadDailyBookmarks();
}
window.DailyBookmarksPage = {
activate,
deactivate: cleanup
};
const dailySection = document.querySelector('[data-view="daily-bookmarks"]');
if (dailySection && dailySection.classList.contains('app-view--active')) {
activate();
}
})();