Blur gallery images by default, sync progress UI, and add chat/monitor tests

- Blur NSFW media gallery images until hover/reveal
- Surface chat sync progress in the dashboard header
- Extend chat service, cache, and monitor with delta-media handling
- Add tests for chat cooldown, delta media, and monitor flows

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
jjbruntonandClaude Opus 4.7 committed 2026-05-22 10:01:09 +01:00
1 parent 633bb0ec04
commit 2260388dc8
15 files changed
+675 -41

No files matched your search

+134 -5
View File
@@ -1506,6 +1506,13 @@
object-fit: cover;
display: block;
background: var(--card-2);
filter: blur(18px);
transition: filter .2s ease;
}
.media-card:hover img,
.media-card.revealed img {
filter: none;
}
.media-meta {
@@ -2268,8 +2275,13 @@
<div class="panels">
<div class="panel" style="grid-column: span 2;">
<div class="panel-header-row">
<div class="panel-header-row" style="flex-wrap: wrap; row-gap: 6px;">
<span class="panel-title">Active chats · last 48 hours</span>
<span id="chat-sync-status-main" class="muted" style="font-size: 12px; margin-left: 12px;"></span>
<div id="sync-progress-bar-main" style="display: none; flex-basis: 100%; height: 4px; background: rgba(255,255,255,0.08); border-radius: 2px; overflow: hidden; margin-top: 4px;">
<div id="sync-progress-fill-main" style="height: 100%; width: 0%; background: var(--accent, #6cf); transition: width 200ms ease;"></div>
</div>
<span style="flex: 1;"></span>
<button class="refresh-btn" onclick="fetchFromReddit()" id="fetch-reddit-btn"
style="background: var(--terracotta); color: #fff8ef; border-color: var(--terracotta-deep);">🌐 Fetch from Reddit</button>
<button class="refresh-btn" onclick="fetchChats()" id="refresh-chats-btn">🔄 Refresh cache</button>
@@ -2455,6 +2467,22 @@
<span class="hint">Random delay added to make timing less predictable</span>
</div>
<h3>Post prioritisation</h3>
<div class="toggle-group">
<label>Prioritise recent posts — prefer fresh posts when scanning</label>
<label class="toggle">
<input type="checkbox" id="prioritizeRecentPosts">
<span class="toggle-slider"></span>
</label>
</div>
<div class="form-group">
<label for="recentWindowMinutes">Recent window (minutes)</label>
<input type="number" id="recentWindowMinutes" min="1" max="1440" value="60">
<span class="hint">Posts newer than this are preferred. Older posts are only used as a fallback when no recent ones exist.</span>
</div>
<h3>Mode</h3>
<div class="toggle-group">
@@ -2857,6 +2885,7 @@
if (!res.ok) return;
const s = await res.json();
const banner = document.getElementById('chat-sync-status');
const bannerMain = document.getElementById('chat-sync-status-main');
const deltaBtn = document.getElementById('delta-sync-btn');
const fullBtn = document.getElementById('full-sync-btn');
if (!banner || !deltaBtn || !fullBtn) return;
@@ -2880,7 +2909,9 @@
}
const bar = document.getElementById('sync-progress-bar');
const barMain = document.getElementById('sync-progress-bar-main');
if (bar) bar.style.display = 'none';
if (barMain) barMain.style.display = 'none';
const parts = [
s.cachedChats + ' chats cached',
@@ -2888,7 +2919,9 @@
'last full: ' + fmtAgo(s.lastFullSyncAt),
];
if (syncing) parts.unshift((s.syncingKind === 'full' ? 'Full' : 'Delta') + ' sync running');
banner.textContent = parts.join(' • ');
const text = parts.join(' • ');
banner.textContent = text;
if (bannerMain) bannerMain.textContent = syncing ? text : '';
} catch (err) {
console.error('chat sync status:', err);
}
@@ -2906,8 +2939,11 @@
function applySyncProgress(progress) {
const banner = document.getElementById('chat-sync-status');
const bannerMain = document.getElementById('chat-sync-status-main');
const bar = document.getElementById('sync-progress-bar');
const barMain = document.getElementById('sync-progress-bar-main');
const fill = document.getElementById('sync-progress-fill');
const fillMain = document.getElementById('sync-progress-fill-main');
const deltaBtn = document.getElementById('delta-sync-btn');
const fullBtn = document.getElementById('full-sync-btn');
if (!banner || !bar || !fill) return;
@@ -2915,6 +2951,9 @@
if (!progress) {
bar.style.display = 'none';
fill.style.width = '0%';
if (barMain) barMain.style.display = 'none';
if (fillMain) fillMain.style.width = '0%';
if (bannerMain) bannerMain.textContent = '';
if (deltaBtn) deltaBtn.disabled = false;
if (fullBtn) fullBtn.disabled = false;
if (deltaBtn) deltaBtn.textContent = 'Delta sync';
@@ -2944,15 +2983,21 @@
parts.push('— ' + progress.label);
}
parts.push('(' + elapsedStr + ')');
banner.textContent = parts.join(' • ');
const text = parts.join(' • ');
banner.textContent = text;
if (bannerMain) bannerMain.textContent = text;
if (progress.total > 0) {
const pct = Math.min(100, (progress.current / progress.total) * 100);
bar.style.display = 'block';
fill.style.width = pct + '%';
if (barMain) barMain.style.display = 'block';
if (fillMain) fillMain.style.width = pct + '%';
} else {
bar.style.display = 'block';
fill.style.width = '5%';
if (barMain) barMain.style.display = 'block';
if (fillMain) fillMain.style.width = '5%';
}
}
@@ -3212,13 +3257,32 @@
`}).join('');
}
let openConversationUsername = null;
let openConversationSyncWatch = null;
async function openConversation(username) {
const modal = document.getElementById('conversation-modal');
const modalTitle = document.getElementById('modal-title');
const modalBody = document.getElementById('modal-body');
openConversationUsername = username;
modalTitle.textContent = `Conversation with u/${username}`;
modalBody.innerHTML = '<div class="empty-state">Loading…</div>';
// If a sync is in flight, set a clearer placeholder up front instead of bare "Loading…".
let placeholderText = 'Loading…';
try {
const sRes = await fetch('/api/chats/sync');
if (sRes.ok) {
const s = await sRes.json();
if (s.syncing && s.progress) {
const phase = SYNC_PHASE_LABELS[s.progress.phase] || s.progress.phase;
const counter = s.progress.total > 0
? ` (${s.progress.current} / ${s.progress.total})`
: '';
placeholderText = `${s.syncingKind === 'full' ? 'Full' : 'Delta'} sync running — ${phase}${counter}. Loading cached messages…`;
}
}
} catch (_) { /* non-blocking */ }
modalBody.innerHTML = `<div class="empty-state">${escapeHtml(placeholderText)}</div>`;
modal.classList.add('active');
try {
@@ -3240,6 +3304,29 @@
}
metaHtml += '</div>';
let syncBannerHtml = '';
if (conv.syncInProgress) {
const sip = conv.syncInProgress;
const phase = sip.progress
? (SYNC_PHASE_LABELS[sip.progress.phase] || sip.progress.phase)
: '';
const counter = sip.progress && sip.progress.total > 0
? ` (${sip.progress.current} / ${sip.progress.total})`
: '';
const kindLabel = sip.kind === 'full' ? 'Full' : 'Delta';
const note = sip.staleMessages
? 'Showing cached messages — newer history will load once sync finishes.'
: 'Newer messages may appear once sync finishes.';
syncBannerHtml = `
<div class="meta-item" style="background: rgba(255,204,102,0.12); border: 1px solid rgba(255,204,102,0.3); padding: 8px 12px; border-radius: 6px; margin-bottom: 8px;">
<strong>${kindLabel} sync in progress</strong> — ${escapeHtml(phase)}${escapeHtml(counter)}. ${escapeHtml(note)}
</div>
`;
startConversationSyncWatch(username);
} else {
stopConversationSyncWatch();
}
let messagesHtml = '';
if (conv.messages && conv.messages.length > 0) {
messagesHtml = conv.messages.map(msg => {
@@ -3287,7 +3374,7 @@
replyHtml = '<div class="no-messages">Waiting for them to reply before you can respond here.</div>';
}
modalBody.innerHTML = metaHtml + messagesHtml + replyHtml;
modalBody.innerHTML = metaHtml + syncBannerHtml + messagesHtml + replyHtml;
} catch (err) {
console.error('Failed to load conversation:', err);
@@ -3380,6 +3467,40 @@
function closeConversationModal() {
document.getElementById('conversation-modal').classList.remove('active');
openConversationUsername = null;
stopConversationSyncWatch();
}
// Poll the sync status while the modal is open under an active sync; when
// the sync ends, re-fetch the conversation so the user sees the freshly
// loaded history without having to close + reopen.
function startConversationSyncWatch(username) {
stopConversationSyncWatch();
openConversationSyncWatch = setInterval(async () => {
if (openConversationUsername !== username) {
stopConversationSyncWatch();
return;
}
try {
const r = await fetch('/api/chats/sync');
if (!r.ok) return;
const s = await r.json();
if (!s.syncing) {
stopConversationSyncWatch();
// Sync finished — reopen to pull fresh history.
if (openConversationUsername === username) {
openConversation(username);
}
}
} catch (_) { /* swallow; next tick will try again */ }
}, 3000);
}
function stopConversationSyncWatch() {
if (openConversationSyncWatch) {
clearInterval(openConversationSyncWatch);
openConversationSyncWatch = null;
}
}
document.getElementById('conversation-modal').addEventListener('click', function (e) {
@@ -3510,6 +3631,10 @@
document.getElementById('minDelayMinutes').value = Math.round(currentConfig.rateLimit.minDelayBetweenMessagesMs / 60000);
document.getElementById('jitterSeconds').value = Math.round(currentConfig.rateLimit.jitterMs / 1000);
document.getElementById('dryRunMode').checked = currentConfig.browser.dryRun;
if (currentConfig.reddit) {
document.getElementById('prioritizeRecentPosts').checked = !!currentConfig.reddit.prioritizeRecentPosts;
document.getElementById('recentWindowMinutes').value = currentConfig.reddit.recentWindowMinutes ?? 60;
}
}
function openSettingsModal() {
@@ -3627,6 +3752,10 @@
},
browser: {
dryRun: document.getElementById('dryRunMode').checked
},
reddit: {
prioritizeRecentPosts: document.getElementById('prioritizeRecentPosts').checked,
recentWindowMinutes: parseInt(document.getElementById('recentWindowMinutes').value, 10)
}
};