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:
1 parent
633bb0ec04
commit
2260388dc8
15 files changed
+675
-41
No files matched your search
+134
-5
@@ -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)
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in new issue
Block a user