refresh redesign
This commit is contained in:
187
web/app.js
187
web/app.js
@@ -34,6 +34,11 @@ let currentProfile = 1;
|
||||
let currentTab = 'pending';
|
||||
let posts = [];
|
||||
let profilePollTimer = null;
|
||||
const UPDATES_RECONNECT_DELAY = 5000;
|
||||
let updatesEventSource = null;
|
||||
let updatesReconnectTimer = null;
|
||||
let updatesStreamHealthy = false;
|
||||
let updatesShouldResyncOnConnect = false;
|
||||
|
||||
const MAX_PROFILES = 5;
|
||||
const PROFILE_NAMES = {
|
||||
@@ -57,6 +62,153 @@ function apiFetch(url, options = {}) {
|
||||
return fetch(url, config);
|
||||
}
|
||||
|
||||
function sortPostsByCreatedAt() {
|
||||
posts.sort((a, b) => toTimestamp(b.created_at, 0) - toTimestamp(a.created_at, 0));
|
||||
}
|
||||
|
||||
function applyPostUpdateFromStream(post) {
|
||||
if (!post || !post.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
const index = posts.findIndex((item) => item.id === post.id);
|
||||
if (index !== -1) {
|
||||
posts[index] = post;
|
||||
} else {
|
||||
posts.push(post);
|
||||
}
|
||||
|
||||
sortPostsByCreatedAt();
|
||||
|
||||
if (manualPostMode === 'edit' && manualPostEditingId === post.id) {
|
||||
populateManualPostForm(post);
|
||||
}
|
||||
|
||||
renderPosts();
|
||||
}
|
||||
|
||||
function removePostFromCache(postId) {
|
||||
if (!postId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const index = posts.findIndex((item) => item.id === postId);
|
||||
if (index === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
posts.splice(index, 1);
|
||||
|
||||
if (
|
||||
manualPostMode === 'edit'
|
||||
&& manualPostEditingId === postId
|
||||
&& manualPostModal
|
||||
&& manualPostModal.classList.contains('open')
|
||||
) {
|
||||
closeManualPostModal();
|
||||
}
|
||||
|
||||
renderPosts();
|
||||
}
|
||||
|
||||
function handleBackendEvent(eventPayload) {
|
||||
if (!eventPayload || typeof eventPayload !== 'object') {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (eventPayload.type) {
|
||||
case 'post-upsert':
|
||||
if (eventPayload.post) {
|
||||
applyPostUpdateFromStream(eventPayload.post);
|
||||
}
|
||||
break;
|
||||
case 'post-deleted':
|
||||
if (eventPayload.postId) {
|
||||
removePostFromCache(eventPayload.postId);
|
||||
}
|
||||
break;
|
||||
case 'connected':
|
||||
case 'heartbeat':
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleUpdatesReconnect() {
|
||||
if (updatesReconnectTimer) {
|
||||
return;
|
||||
}
|
||||
updatesReconnectTimer = setTimeout(() => {
|
||||
updatesReconnectTimer = null;
|
||||
startUpdatesStream();
|
||||
}, UPDATES_RECONNECT_DELAY);
|
||||
}
|
||||
|
||||
function startUpdatesStream() {
|
||||
if (typeof EventSource === 'undefined') {
|
||||
console.warn('EventSource wird von diesem Browser nicht unterstützt. Fallback auf Polling.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (updatesEventSource) {
|
||||
return;
|
||||
}
|
||||
|
||||
const eventsUrl = `${API_URL}/events`;
|
||||
let eventSource;
|
||||
|
||||
try {
|
||||
eventSource = new EventSource(eventsUrl, { withCredentials: true });
|
||||
} catch (error) {
|
||||
console.warn('Konnte Update-Stream nicht starten:', error);
|
||||
scheduleUpdatesReconnect();
|
||||
return;
|
||||
}
|
||||
|
||||
updatesEventSource = eventSource;
|
||||
|
||||
eventSource.addEventListener('open', () => {
|
||||
updatesStreamHealthy = true;
|
||||
if (updatesReconnectTimer) {
|
||||
clearTimeout(updatesReconnectTimer);
|
||||
updatesReconnectTimer = null;
|
||||
}
|
||||
if (updatesShouldResyncOnConnect) {
|
||||
updatesShouldResyncOnConnect = false;
|
||||
fetchPosts({ showLoader: false });
|
||||
}
|
||||
applyAutoRefreshSettings();
|
||||
});
|
||||
|
||||
eventSource.addEventListener('message', (event) => {
|
||||
if (!event || typeof event.data !== 'string' || !event.data.trim()) {
|
||||
return;
|
||||
}
|
||||
let payload;
|
||||
try {
|
||||
payload = JSON.parse(event.data);
|
||||
} catch (error) {
|
||||
console.warn('Ungültige Daten vom Update-Stream erhalten:', error);
|
||||
return;
|
||||
}
|
||||
handleBackendEvent(payload);
|
||||
});
|
||||
|
||||
eventSource.addEventListener('error', () => {
|
||||
if (updatesEventSource) {
|
||||
updatesEventSource.close();
|
||||
updatesEventSource = null;
|
||||
}
|
||||
if (!updatesShouldResyncOnConnect) {
|
||||
updatesShouldResyncOnConnect = true;
|
||||
}
|
||||
updatesStreamHealthy = false;
|
||||
applyAutoRefreshSettings();
|
||||
fetchPosts({ showLoader: false });
|
||||
scheduleUpdatesReconnect();
|
||||
});
|
||||
}
|
||||
|
||||
const screenshotModal = document.getElementById('screenshotModal');
|
||||
const screenshotModalContent = document.getElementById('screenshotModalContent');
|
||||
const screenshotModalImage = document.getElementById('screenshotModalImage');
|
||||
@@ -131,7 +283,7 @@ function initializeFocusParams() {
|
||||
|
||||
let autoRefreshTimer = null;
|
||||
let autoRefreshSettings = {
|
||||
enabled: true,
|
||||
enabled: false,
|
||||
interval: 30000
|
||||
};
|
||||
let sortMode = DEFAULT_SORT_SETTINGS.mode;
|
||||
@@ -2106,10 +2258,22 @@ function applyAutoRefreshSettings() {
|
||||
autoRefreshTimer = null;
|
||||
}
|
||||
|
||||
if (autoRefreshIntervalSelect) {
|
||||
const disabled = !autoRefreshSettings.enabled || updatesStreamHealthy;
|
||||
autoRefreshIntervalSelect.disabled = disabled;
|
||||
autoRefreshIntervalSelect.title = updatesStreamHealthy
|
||||
? 'Live-Updates sind aktiv; das Intervall wird aktuell nicht verwendet.'
|
||||
: '';
|
||||
}
|
||||
|
||||
if (!autoRefreshSettings.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (updatesStreamHealthy) {
|
||||
return;
|
||||
}
|
||||
|
||||
autoRefreshTimer = setInterval(() => {
|
||||
if (document.hidden) {
|
||||
return;
|
||||
@@ -2485,12 +2649,12 @@ if (manualPostResetButton) {
|
||||
if (autoRefreshToggle) {
|
||||
autoRefreshToggle.addEventListener('change', () => {
|
||||
autoRefreshSettings.enabled = !!autoRefreshToggle.checked;
|
||||
if (autoRefreshIntervalSelect) {
|
||||
autoRefreshIntervalSelect.disabled = !autoRefreshSettings.enabled;
|
||||
}
|
||||
saveAutoRefreshSettings();
|
||||
applyAutoRefreshSettings();
|
||||
if (autoRefreshSettings.enabled) {
|
||||
if (autoRefreshSettings.enabled && updatesStreamHealthy) {
|
||||
console.info('Live-Updates sind aktiv; automatisches Refresh bleibt pausiert.');
|
||||
}
|
||||
if (autoRefreshSettings.enabled && !updatesStreamHealthy) {
|
||||
fetchPosts({ showLoader: false });
|
||||
}
|
||||
});
|
||||
@@ -2578,6 +2742,7 @@ async function fetchPosts({ showLoader = true } = {}) {
|
||||
const data = await response.json();
|
||||
posts = Array.isArray(data) ? data : [];
|
||||
await normalizeLoadedPostUrls();
|
||||
sortPostsByCreatedAt();
|
||||
renderPosts();
|
||||
} catch (error) {
|
||||
if (showLoader) {
|
||||
@@ -3068,15 +3233,20 @@ function createPostCard(post, status, meta = {}) {
|
||||
const createdDate = formatDateTime(post.created_at) || '—';
|
||||
const lastChangeDate = formatDateTime(post.last_change || post.created_at) || '—';
|
||||
|
||||
const resolvedScreenshotPath = post.screenshot_path
|
||||
const baseScreenshotPath = post.screenshot_path
|
||||
? (post.screenshot_path.startsWith('http')
|
||||
? post.screenshot_path
|
||||
: `${API_URL.replace(/\/api$/, '')}${post.screenshot_path.startsWith('/') ? '' : '/'}${post.screenshot_path}`)
|
||||
: `${API_URL}/posts/${post.id}/screenshot`;
|
||||
|
||||
const screenshotVersion = post.last_change || post.updated_at || post.created_at || '';
|
||||
const versionedScreenshotPath = screenshotVersion
|
||||
? `${baseScreenshotPath}${baseScreenshotPath.includes('?') ? '&' : '?'}v=${encodeURIComponent(screenshotVersion)}`
|
||||
: baseScreenshotPath;
|
||||
const resolvedScreenshotPath = versionedScreenshotPath;
|
||||
const screenshotHtml = `
|
||||
<div class="post-screenshot" data-screenshot="${escapeHtml(resolvedScreenshotPath)}" role="button" tabindex="0" aria-label="Screenshot anzeigen">
|
||||
<img src="${escapeHtml(resolvedScreenshotPath)}" alt="Screenshot zum Beitrag" loading="lazy" />
|
||||
<div class="post-screenshot" data-screenshot="${escapeHtml(versionedScreenshotPath)}" role="button" tabindex="0" aria-label="Screenshot anzeigen">
|
||||
<img src="${escapeHtml(versionedScreenshotPath)}" alt="Screenshot zum Beitrag" loading="lazy" />
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -3862,4 +4032,5 @@ loadProfile();
|
||||
startProfilePolling();
|
||||
fetchPosts();
|
||||
checkAutoCheck();
|
||||
startUpdatesStream();
|
||||
applyAutoRefreshSettings();
|
||||
|
||||
Reference in New Issue
Block a user