sprint1: agent queue worker, proxy support, auto-mark-on-navigate-away, test fixes

This commit is contained in:
Gan, Jimmy
2026-07-08 22:05:05 +08:00
parent a154bc7978
commit ffa47a1335
8 changed files with 429 additions and 28 deletions
+78 -11
View File
@@ -9,6 +9,9 @@
let syncMessage = $state('');
let pendingCount = $state(0);
let theme = $state(localStorage.getItem('theme') || 'dark');
let apiToken = $state(localStorage.getItem('t-yt-token') || '');
let apiTokenInput = $state('');
let showTokenSetup = $state(false);
let summaryPanel = $state(null);
let notes = $state({});
let activeNote = $state(null);
@@ -17,6 +20,7 @@
let channelFilter = $state('');
let allChannels = $state([]);
let editingCategories = $state(false);
let prevSelectedId = $state(null);
// Agent queue state
let agentItems = $state([]);
@@ -93,7 +97,7 @@
isSyncing = true;
syncMessage = 'Syncing...';
try {
const res = await fetch('/api/videos/fetch', { method: 'POST' });
const res = await fetch('/api/videos/fetch', { method: 'POST', headers: getAuthHeaders() });
if (res.ok) {
syncMessage = 'Sync started...';
setTimeout(() => { loadVideos(); isSyncing = false; syncMessage = ''; }, 6000);
@@ -105,7 +109,7 @@
}
async function updateStatus(videoId, newStatus, silent = false) {
await fetch(`/api/videos/${videoId}/status`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ status: newStatus }) });
await fetch(`/api/videos/${videoId}/status`, { method: 'POST', headers: getAuthHeaders({ 'Content-Type': 'application/json' }), body: JSON.stringify({ status: newStatus }) });
if (!silent) {
videos = videos.filter(v => v.video_id !== videoId);
if (selectedVideo?.video_id === videoId) selectedVideo = videos[0] || null;
@@ -115,7 +119,7 @@
async function generateSummary(videoId) {
if (selectedVideo?.video_id === videoId) selectedVideo.summary = "Generating...";
await fetch(`/api/videos/${videoId}/summarize`, { method: 'POST' });
await fetch(`/api/videos/${videoId}/summarize`, { method: 'POST', headers: getAuthHeaders() });
setTimeout(() => loadVideos(), 3000);
}
@@ -144,7 +148,7 @@
for (const k of missing) payload[k] = local[k];
await fetch('/api/notes/migrate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
headers: getAuthHeaders({ 'Content-Type': 'application/json' }),
body: JSON.stringify({ notes: payload })
});
await loadNotes();
@@ -160,7 +164,7 @@
// Save to server
await fetch(`/api/notes/${vid}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
headers: getAuthHeaders({ 'Content-Type': 'application/json' } ),
body: JSON.stringify({ note_text: current, queue_to_agent: queueToAgent })
});
// Update local state
@@ -181,21 +185,58 @@
}
async function removeAgentItem(id) {
await fetch(`/api/agent-queue/${id}`, { method: 'DELETE' });
await fetch(`/api/agent-queue/${id}`, { method: 'DELETE', headers: getAuthHeaders() });
agentItems = agentItems.filter(i => i.id !== id);
}
async function initAuth() {
// Check if auth is enabled on the backend
try {
const res = await fetch('/api/config');
if (res.ok) {
const config = await res.json();
if (config.auth_enabled && !apiToken) {
showTokenSetup = true;
return false;
}
}
} catch(e) {}
showTokenSetup = false;
return true;
}
async function setupToken() {
if (apiTokenInput.trim()) {
apiToken = apiTokenInput.trim();
localStorage.setItem('t-yt-token', apiToken);
showTokenSetup = false;
}
}
function getAuthHeaders(extraHeaders = {}) {
const headers = { ...extraHeaders };
if (apiToken) {
headers['Authorization'] = 'Bearer ' + apiToken;
}
return headers;
}
// ---- Lifecycle ----
$effect(() => { if (activeTab) loadVideos(); });
$effect(() => {
// Mark previous video as read when navigating away
if (selectedVideo && summaryPanel) {
tick().then(() => summaryPanel.scrollTop = 0);
const vid = selectedVideo.video_id;
const hasSummary = selectedVideo.summary && !selectedVideo.summary.includes('Error') && !selectedVideo.summary.includes('📺 Short clip');
if (hasSummary && activeTab === 'pending') {
const timer = setTimeout(() => updateStatus(vid, 'read', true), 3000);
return () => clearTimeout(timer);
}
const prevId = prevSelectedId;
prevSelectedId = selectedVideo?.video_id || null;
if (prevId && prevId !== selectedVideo?.video_id && activeTab === 'pending') {
// Look up the previous video to check it had a valid summary
const prevVideo = videos.find(v => v.video_id === prevId);
if (prevVideo && prevVideo.summary && !prevVideo.summary.includes('Error') && !prevVideo.summary.includes('📺 Short clip')) {
updateStatus(prevId, 'read', true);
}
}
});
@@ -205,6 +246,7 @@
onMount(async () => {
document.documentElement.className = theme;
await initAuth();
await loadNotes();
// Migrate localStorage notes to server (non-blocking)
migrateLocalNotes();
@@ -323,6 +365,31 @@
</script>
<div style="background:var(--bg-primary);color:var(--text-primary);height:100vh;overflow:hidden;display:flex;flex-direction:column">
<!-- Token Setup Modal -->
{#if showTokenSetup}
<div class="fixed inset-0 z-50 flex items-center justify-center" style="background:rgba(0,0,0,0.5)">
<div class="bg-[var(--bg-secondary)] p-6 rounded-lg max-w-md w-full mx-4"
style="border:1px solid var(--border)">
<h2 class="text-xl font-bold mb-2">API Token Required</h2>
<p class="text-sm mb-4" style="color:var(--text-secondary)">
This app requires an API token to save notes and manage videos.
Please enter the token configured on the server.
</p>
<input type="text" bind:value={apiTokenInput}
placeholder="Enter API token..." class="w-full px-3 py-2 rounded"
style="background:var(--bg-primary);border:1px solid var(--border);color:var(--text-primary)"
onkeydown={(e) => e.key === 'Enter' && setupToken()} />
<div class="flex space-x-2 mt-4">
<button onclick={setupToken}
class="px-4 py-2 rounded text-white font-bold"
style="background:var(--accent)">
Submit
</button>
</div>
</div>
</div>
{/if}
<header style="background:var(--bg-secondary);border-bottom:1px solid var(--border);" class="flex items-center justify-between px-5 py-2.5">
<div class="flex items-center space-x-3">
<span class="text-lg font-black tracking-wider" style="color:var(--accent)">T YouTube</span>