sprint1: agent queue worker, proxy support, auto-mark-on-navigate-away, test fixes
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user