old app_core
Modified: February 14, 2026 7:58 PM Created: January 6, 2026 5:49 PM Master Type: Notes Hide: No Starred: No Status: Unassigned
/**
* AppCore — NovaWeb-Aligned
*/
class CatalogueApp {
constructor() {
this.selectedIds = new Set();
this.lastSelectedId = null;
this.focusId = null;
this.bucket = new Set();
this.focusMode = false;
this.focusIdleMinutes = 10;
this.focusTimer = null;
this.focusClockTimer = null;
this.focusOverlay = null;
this.focusTasks = null;
this.focusTaskIndex = 0;
this.taskRotationTick = 0;
this.lastTaskMinute = null;
document.addEventListener('DOMContentLoaded', () => {
const isCollapsed = localStorage.getItem('novaweb_bucket_collapsed') === 'true';
const bucketHud = document.getElementById('bucket-hud');
if (isCollapsed && bucketHud) {
bucketHud.classList.add('collapsed');
}
this.init();
});
document.addEventListener('keydown', e => this.handleKeydown(e));
// Activity resets focus timer
['mousemove','mousedown','wheel','touchstart'].forEach(evt =>
document.addEventListener(evt, () => this.resetFocusTimer(), { passive: true })
);
}
init() {
// WHISKERS STARTUP - Consolidated Gatekeeper
const whiskers = document.getElementById('whiskers-bar');
if (whiskers) {
// Set the scroll duration
whiskers.style.animation = 'whiskers-scroll 25s linear infinite';
// This event listener handles the swap ONLY when text is off-screen
whiskers.addEventListener('animationiteration', () => {
if (whiskers.dataset.pendingText) {
whiskers.innerText = whiskers.dataset.pendingText;
whiskers.dataset.pendingText = "";
}
});
}
this.syncState();
this.updateSelectionUI();
this.autoRefreshPreviews(); // Start the background loader
}
// ============================================================
// Focus Mode
// ============================================================
initFocusOverlay() {
const overlay = document.createElement('div');
overlay.id = 'focus-overlay';
overlay.style.display = 'none';
overlay.innerHTML = `
<div class="focus-stack">
<div id="focus-time" class="focus-time"></div>
<div id="focus-date" class="focus-date"></div>
<div id="focus-whiskers" class="focus-whiskers"></div>
<div id="focus-task" class="focus-task"></div>
</div>
`;
document.body.appendChild(overlay);
this.focusOverlay = overlay;
fetch('/api/focus_tasks')
.then(r => r.ok ? r.json() : null)
.then(j => { this.focusTasks = j; })
.catch(() => { this.focusTasks = null; });
}
enterFocusMode() {
if (this.focusMode) return;
this.focusMode = true;
document.body.classList.add('focus-mode');
if (this.focusOverlay) this.focusOverlay.style.display = 'flex';
this.updateFocusOverlay();
// 1s clock for UI updates + Decoupled Task Rotation Check
this.focusClockTimer = setInterval(() => {
this.updateFocusOverlay();
const now = new Date();
const currentMin = now.getMinutes();
// DECOUPLED: Task rotation is tied to minute-boundary crossing, not a fixed system clock schedule
if (this.lastTaskMinute !== null && currentMin !== this.lastTaskMinute) {
this.taskRotationTick++;
if (this.taskRotationTick % 1 === 0) {
this.rotateFocusTask();
}
}
this.lastTaskMinute = currentMin;
}, 1000);
}
exitFocusMode() {
if (!this.focusMode) return;
this.focusMode = false;
document.body.classList.remove('focus-mode');
if (this.focusOverlay) this.focusOverlay.style.display = 'none';
clearInterval(this.focusClockTimer);
clearTimeout(this.focusTaskTimer);
this.focusClockTimer = null;
this.focusTaskTimer = null;
this.resetFocusTimer();
}
resetFocusTimer() {
if (this.focusMode) { this.exitFocusMode(); return; }
if (this.focusTimer) clearTimeout(this.focusTimer);
this.focusTimer = setTimeout(
() => this.enterFocusMode(),
this.focusIdleMinutes * 60 * 1000
);
}
rotateFocusTask() {
const taskEl = document.getElementById('focus-task');
if (!taskEl || !this.focusTasks) return;
const pool =
(this.focusTasks.today && this.focusTasks.today.length) ? this.focusTasks.today :
(this.focusTasks.notes && this.focusTasks.notes.length) ? this.focusTasks.notes :
[];
if (!pool.length) return;
taskEl.innerText = pool[this.focusTaskIndex % pool.length];
this.focusTaskIndex++;
}
updateFocusOverlay() {
const now = new Date();
const timeEl = document.getElementById('focus-time');
const dateEl = document.getElementById('focus-date');
const whiskEl = document.getElementById('focus-whiskers');
if (timeEl) {
timeEl.innerText = now.toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit'
});
}
if (dateEl) {
dateEl.innerText = now.toLocaleDateString(undefined, {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric'
});
}
if (whiskEl) {
const bar = document.getElementById('whiskers-bar');
whiskEl.innerText = bar ? bar.innerText : '';
whiskEl.style.color = bar ? bar.style.color : 'var(--accent)';
}
}
autoRefreshPreviews() {
const placeholders = Array.from(document.querySelectorAll('.asset-card'))
.filter(card => card.innerText.includes('Generating...'));
if (placeholders.length > 0) {
placeholders.forEach(card => {
const id = card.dataset.id;
const imgUrl = `/preview/${encodeURIComponent(id)}.jpg`;
const imgTest = new Image();
imgTest.onload = () => {
const hitbox = card.querySelector('.preview-hitbox');
const placeholder = hitbox.querySelector('div:not(.badge):not(.label-dot)');
if (placeholder) {
const img = document.createElement('img');
img.src = imgUrl;
img.className = "w-full h-full object-contain transition-transform duration-500 group-hover:scale-110";
img.setAttribute('loading', 'lazy');
hitbox.replaceChild(img, placeholder);
}
};
imgTest.src = imgUrl;
});
}
// Check every 5 seconds
setTimeout(() => this.autoRefreshPreviews(), 5000);
}
toggleBucket() {
const bucket = document.getElementById('bucket-hud');
if (!bucket) return;
bucket.classList.toggle('collapsed');
localStorage.setItem('novaweb_bucket_collapsed', bucket.classList.contains('collapsed'));
}
applyLabel(labelName) {
const targets = this.selectedIds.size ? Array.from(this.selectedIds) : (this.focusId ? [this.focusId] : []);
if (!targets.length) return;
targets.forEach(id => {
fetch('/api/label', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ id: id, label: labelName })
}).then(() => {
const card = document.querySelector(`.asset-card[data-id="${id}"]`);
if (card) {
card.dataset.label = labelName;
const previewHitbox = card.querySelector('.preview-hitbox');
let dot = card.querySelector('.label-dot');
if (labelName === 'none' || !labelName) {
if (dot) dot.remove();
} else {
if (!dot) {
dot = document.createElement('div');
previewHitbox.appendChild(dot);
}
dot.className = `label-dot label-${labelName}`;
}
}
});
});
this.toast(`Applied ${labelName.toUpperCase()} label`);
}
filterByLabel(labelName) {
const cards = document.querySelectorAll('.asset-card');
cards.forEach(card => {
if (labelName === 'all') {
card.style.display = '';
} else {
card.style.display = card.dataset.label === labelName ? '' : 'none';
}
});
this.updateStats();
}
clearFlags() {
if (this.selectedIds.size > 0) {
this.batchRating('0');
} else if (this.focusId) {
this.submitRating(this.focusId, '0');
}
this.toast("FLAGS CLEARED");
}
runJob(key) {
// Requirement 3: Separate "Selected" vs "Everything" logic
const ids = this.selectedIds.size > 0 ? Array.from(this.selectedIds) : [];
const modeLabel = ids.length > 0 ? `SELECTED ASSETS (${ids.length})` : "ALL ASSETS";
fetch('/api/run_action', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ key, ids })
}).then(() => {
this.toast(`${key.toUpperCase()} INITIATED ON ${modeLabel}`);
// Only clear selection if we actually used it for the job
if (ids.length > 0) this.clearSelection();
});
}
handleKeydown(e) {
if (!e || ['INPUT','TEXTAREA','SELECT'].includes(e.target.tagName)) return;
const k = (e.key || '').toLowerCase();
if (k === '0') {
e.preventDefault();
this.clearFlags();
return;
}
if (this.focusMode) {
this.exitFocusMode();
return;
}
if (k === 'f') {
e.preventDefault();
this.enterFocusMode();
return;
}
if (k === '/') { e.preventDefault(); document.getElementById('search')?.focus(); return; }
if (k === 'escape') { this.closeLightbox(); this.clearSelection(); return; }
if (k === ' ') {
e.preventDefault();
const lb = document.getElementById('lb');
if (lb && !lb.classList.contains('hidden')) {
this.closeLightbox();
} else if (this.focusId) {
this.openLightbox(this.focusId);
}
return;
}
if (k === 'o') { this.runJob('organize'); return; }
const ratingMap = {
'1': 'pass', 'n': 'pass',
'2': 'fix', 'm': 'fix',
'3': 'skip', 's': 'skip',
'4': 'reshoot',
'5': 'best',
'6': 'other',
'0': '0'
};
if (ratingMap[k]) {
const action = ratingMap[k];
if (this.focusId && !this.selectedIds.size) {
this.rateAndAdvance(action);
} else if (this.selectedIds.size) {
this.batchRating(action);
}
return;
}
if (k === 'j') { this.jumpNextUnrated(); return; }
if (k === 't') { this.scrollToTop(); return; }
if (k === 'b') { this.scrollToBottom(); return; }
if ((e.metaKey || e.ctrlKey) && k === 'a') {
e.preventDefault();
document.querySelectorAll('.asset-card').forEach(c => {
if (c.style.display !== 'none') this.selectedIds.add(c.dataset.id);
});
this.updateSelectionUI();
}
if (e.key === 'ArrowRight' && this.focusId) { e.preventDefault(); this.navigateLightbox(1); return; }
if (e.key === 'ArrowLeft' && this.focusId) { e.preventDefault(); this.navigateLightbox(-1); return; }
}
submitRating(id, rating) {
if (!id) return;
fetch('/api/rate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id, rating })
}).then(() => {
const card = document.querySelector(`.asset-card[data-id="${id}"]`);
if (card) {
card.dataset.rating = encodeURIComponent(rating);
card.classList.remove('state-pass','state-fix','state-skip','state-reshoot', 'state-best', 'state-other');
card.classList.add(`state-${rating}`);
}
if (rating === 'fix' || rating === 'reshoot') this.addToBucket(id);
else this.removeFromBucket(id);
this.toast(`SET: ${rating.toUpperCase()}`);
this.updateStats();
});
}
rateAndAdvance(status) {
const id = this.focusId;
if (!id) return;
fetch('/api/rate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id, rating: status })
}).then(() => {
const card = document.querySelector(`.asset-card[data-id="${id}"]`);
if (card) {
card.dataset.rating = encodeURIComponent(status);
card.classList.remove('state-pass','state-fix','state-skip','state-reshoot', 'state-best', 'state-other');
card.classList.add(`state-${status}`);
}
if (status === 'fix' || status === 'reshoot') this.addToBucket(id);
else this.removeFromBucket(id);
const cards = Array.from(document.querySelectorAll('.asset-card')).filter(c => c.style.display !== 'none');
const ids = cards.map(c => c.dataset.id);
const i = ids.indexOf(id);
const next = ids[i + 1];
if (!next) { this.closeLightbox(); return; }
this.focusId = next;
const img = document.getElementById('lb-img');
if (img) img.src = `/preview-hi/${encodeURIComponent(next)}.jpg`;
this.updateStats();
});
}
batchRating(status) {
if (!this.selectedIds.size) return;
const tasks = Array.from(this.selectedIds).map(id =>
fetch('/api/rate', { method: 'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ id, rating: status }) })
);
Promise.all(tasks).then(() => {
this.selectedIds.forEach(id => {
if (status === 'fix' || status === 'reshoot') this.addToBucket(id);
else this.removeFromBucket(id);
const card = document.querySelector(`.asset-card[data-id="${id}"]`);
if (card) {
card.dataset.rating = encodeURIComponent(status);
card.classList.remove('state-pass','state-fix','state-skip','state-reshoot', 'state-best', 'state-other');
card.classList.add(`state-${status}`);
}
});
this.clearSelection();
this.toast(`BATCH: ${status.toUpperCase()}`);
this.updateStats();
});
}
handleCardClick(id, e) {
if (e?.stopPropagation) e.stopPropagation();
if (e?.shiftKey && this.lastSelectedId) {
const cards = Array.from(document.querySelectorAll('.asset-card'))
.filter(c => c.style.display !== 'none').map(c => c.dataset.id);
const a = cards.indexOf(this.lastSelectedId);
const b = cards.indexOf(id);
if (a !== -1 && b !== -1) {
cards.slice(Math.min(a,b), Math.max(a,b)+1).forEach(x => this.selectedIds.add(x));
}
} else {
this.selectedIds.has(id) ? this.selectedIds.delete(id) : this.selectedIds.add(id);
}
this.lastSelectedId = id;
this.focusId = id;
this.updateSelectionUI();
}
updateSelectionUI() {
document.querySelectorAll('.asset-card').forEach(card => {
const sel = this.selectedIds.has(card.dataset.id);
card.classList.toggle('selected', sel);
});
const sc = document.getElementById('sel-count');
if (sc) sc.innerText = this.selectedIds.size;
document.getElementById('selection-toolbar')?.classList.toggle('active', this.selectedIds.size > 0);
}
clearSelection() {
this.selectedIds.clear();
this.updateSelectionUI();
}
openLightbox(id) {
const lb = document.getElementById('lb');
const img = document.getElementById('lb-img');
if (!lb || !img) return;
img.src = `/preview-hi/${encodeURIComponent(id)}.jpg`;
lb.classList.remove('hidden');
requestAnimationFrame(() => {
requestAnimationFrame(() => {
lb.classList.remove('opacity-0');
});
});
this.focusId = id;
this.updateStats();
}
closeLightbox() {
const lb = document.getElementById('lb');
if (!lb) return;
lb.classList.add('opacity-0');
setTimeout(() => {
lb.classList.add('hidden');
const img = document.getElementById('lb-img');
if (img) img.src = '';
}, 200);
}
navigateLightbox(delta) {
const cards = Array.from(document.querySelectorAll('.asset-card')).filter(c => c.style.display !== 'none');
const ids = cards.map(c => c.dataset.id);
const i = ids.indexOf(this.focusId);
const next = ids[i + delta];
if (!next) return;
this.focusId = next;
const img = document.getElementById('lb-img');
if (img) img.src = `/preview-hi/${encodeURIComponent(next)}.jpg`;
this.updateStats();
}
jumpNextUnrated() {
const cards = Array.from(document.querySelectorAll('.asset-card')).filter(c => c.style.display !== 'none');
for (const c of cards) {
const raw = decodeURIComponent(c.dataset.rating || '').toLowerCase().trim();
if (!raw || raw === '0') {
c.scrollIntoView({ behavior:'smooth', block:'center' });
this.focusId = c.dataset.id;
this.toast('NEXT UNRATED');
return;
}
}
this.toast('NO UNRATED');
}
scrollToTop() {
const main = document.querySelector('main');
main ? main.scrollTo({ top:0, behavior:'smooth' }) : window.scrollTo({ top:0, behavior:'smooth' });
}
scrollToBottom() {
const main = document.querySelector('main');
main ? main.scrollTo({ top:main.scrollHeight, behavior:'smooth' }) : window.scrollTo({ top:document.body.scrollHeight, behavior:'smooth' });
}
addToBucket(id) {
if (!id) return;
this.bucket.add(id);
this.renderBucket();
}
removeFromBucket(id) {
this.bucket.delete(id);
this.renderBucket();
}
renderBucket() {
const list = document.getElementById('bucket-list');
if (!list) return;
list.innerHTML = '';
this.bucket.forEach(id => {
const row = document.createElement('div');
row.className = 'bucket-item flex items-center gap-3 p-2 hover:bg-white/5 cursor-pointer rounded-lg';
row.innerHTML = `<img src="/preview/${encodeURIComponent(id)}.jpg" class="w-10 h-10 rounded-md object-cover opacity-80"><div class="text-[10px] truncate flex-1 font-mono opacity-70">${id}</div>`;
row.onclick = () => this.openLightbox(id);
list.appendChild(row);
});
const count = document.getElementById('bucket-count');
if (count) count.innerText = this.bucket.size;
document.getElementById('bucket-hud')?.classList.toggle('has-items', this.bucket.size > 0);
}
syncState() {
fetch('/api/status').then(r=>r.json()).then(s=>{
const bar = document.getElementById('whiskers-bar');
if (bar) {
bar.innerText = s.whiskers_text || 'NOMINAL';
bar.style.color = s.whiskers_color || 'var(--accent)';
}
const prog = document.getElementById('job-prog');
if (prog) {
prog.style.width = (s.job_progress || 0) + '%';
}
setTimeout(()=>this.syncState(), 30000);
}).catch(()=>setTimeout(()=>this.syncState(), 40000));
}
updateStats() {
const visible = Array.from(document.querySelectorAll('.asset-card')).filter(c => c.style.display !== 'none').length;
const visEl = document.getElementById('stat-visible');
if (visEl) visEl.innerText = visible;
const lb = document.getElementById('lb');
if (lb && !lb.classList.contains('hidden')) {
const cards = Array.from(document.querySelectorAll('.asset-card')).filter(c => c.style.display !== 'none');
const idx = cards.findIndex(c => c.dataset.id === this.focusId);
document.getElementById('lb-current-idx').innerText = idx + 1;
document.getElementById('lb-total-count').innerText = cards.length;
}
}
toast(msg) {
const portal = document.getElementById('toast-portal');
if (!portal) return;
const t = document.createElement('div');
t.className = "bg-panel border border-accent/40 text-accent px-6 py-4 rounded-2xl shadow-2xl text-[11px] font-bold mb-2";
t.innerText = String(msg||'');
portal.appendChild(t);
setTimeout(()=>{ t.style.opacity='0'; setTimeout(()=>t.remove(), 500); }, 4000);
}
}
window.App = new CatalogueApp();