diff --git a/src/knowmarks/web/static/app.js b/src/knowmarks/web/static/app.js index 712076d..bd8739c 100644 --- a/src/knowmarks/web/static/app.js +++ b/src/knowmarks/web/static/app.js @@ -3074,9 +3074,18 @@ function renderConnectorsPage() { if (!activeTypes.has('raindrop_api')) availableConnectors.push({name: 'Raindrop.io', icon: 'droplet', desc: 'Sync Raindrop bookmarks', action: 'showConnectorRaindropSetup()'}); if (!activeTypes.has('karakeep_api')) availableConnectors.push({name: 'Karakeep', icon: 'bookmark-check', desc: 'Connect Karakeep instance', action: 'showConnectorKarakeepSetup()'}); if (!activeTypes.has('readwise_reader')) availableConnectors.push({name: 'Readwise Reader', icon: 'book-open', desc: 'Sync documents + highlights', action: 'showConnectorReadwiseSetup()'}); + if (!activeTypes.has('hackernews')) availableConnectors.push({name: 'Hacker News', icon: 'flame', desc: 'Import favorites or submissions', action: 'showConnectorHNSetup()'}); + if (!activeTypes.has('reddit_saved')) availableConnectors.push({name: 'Reddit', icon: 'message-circle', desc: 'Sync saved posts', action: 'showConnectorRedditSetup()'}); + if (!activeTypes.has('linkwarden_api')) availableConnectors.push({name: 'Linkwarden', icon: 'link', desc: 'Connect Linkwarden instance', action: 'showConnectorLinkwardenSetup()'}); + if (!activeTypes.has('linkding_api')) availableConnectors.push({name: 'Linkding', icon: 'bookmark', desc: 'Connect Linkding instance', action: 'showConnectorLinkdingSetup()'}); + if (!activeTypes.has('shiori_api')) availableConnectors.push({name: 'Shiori', icon: 'archive', desc: 'Connect Shiori instance', action: 'showConnectorShioriSetup()'}); + if (!activeTypes.has('wallabag_api')) availableConnectors.push({name: 'Wallabag', icon: 'package', desc: 'Connect Wallabag instance', action: 'showConnectorWallabagSetup()'}); let html = ''; + // Connector types that support credential reconfiguration + const configurableTypes = new Set(['karakeep_api', 'readwise_reader', 'raindrop_api', 'hackernews', 'reddit_saved', 'linkwarden_api', 'linkding_api', 'shiori_api', 'wallabag_api']); + // --- Active Connectors --- if (apiConnectors.length || feeds.length) { html += `
@@ -3086,6 +3095,7 @@ function renderConnectorsPage() { const statusClass = c.health_status === 'ok' ? 'vitality-good' : 'vitality-bad'; const syncInfo = c.last_sync_at ? `synced ${timeAgo(c.last_sync_at)}` : 'never synced'; const schedule = c.sync_schedule ? ` · every ${c.sync_schedule}` : ''; + const canConfigure = configurableTypes.has(c.type); return `
${statusIcon} ${esc(c.name)} @@ -3095,9 +3105,11 @@ function renderConnectorsPage() { ${c.health_message && c.health_status !== 'ok' ? `
${esc(c.health_message)}
` : ''}
${c.health_status !== 'ok' ? `` : ''} + ${canConfigure ? `` : ''}
`; }).join('')}
+
`; } @@ -3282,6 +3294,379 @@ async function connectConnectorReadwise() { } window.connectConnectorReadwise = connectConnectorReadwise; +// --- Hacker News connector --- +function showConnectorHNSetup() { + _connectorInlineSetup('Connect to Hacker News', 'Enter your HN username to import your favorited stories', + [{id: 'username', placeholder: 'your_hn_username'}], + 'connectConnectorHN()'); +} +window.showConnectorHNSetup = showConnectorHNSetup; + +async function connectConnectorHN() { + const username = document.getElementById('cis-username')?.value.trim(); + const status = document.getElementById('cis-status'); + if (!username) { status.textContent = 'Username is required.'; return; } + status.textContent = 'Connecting...'; + try { + const resp = await fetch('/api/connector/hackernews', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({username, mode: 'favorites', schedule: '1d'}) }); + const data = await resp.json(); + if (data.error || data.error_message) { status.textContent = data.error || data.error_message; return; } + showToast(`Hacker News: ${data.new_items} items imported`); + setTimeout(() => showConnectorsPage(), 1500); + } catch (e) { status.textContent = 'Failed: ' + e.message; } +} +window.connectConnectorHN = connectConnectorHN; + +// --- Reddit connector --- +function showConnectorRedditSetup() { + _connectorInlineSetup('Connect to Reddit', 'Enter your Reddit username and session cookie to sync saved posts. Get the cookie value from your browser\'s devtools (Application → Cookies → reddit_session).', + [{id: 'username', placeholder: 'your_reddit_username'}, {id: 'session', type: 'password', placeholder: 'reddit_session cookie value'}], + 'connectConnectorReddit()'); +} +window.showConnectorRedditSetup = showConnectorRedditSetup; + +async function connectConnectorReddit() { + const username = document.getElementById('cis-username')?.value.trim(); + const session = document.getElementById('cis-session')?.value.trim(); + const status = document.getElementById('cis-status'); + if (!username || !session) { status.textContent = 'Username and session cookie are required.'; return; } + status.textContent = 'Connecting...'; + try { + const resp = await fetch('/api/connector/reddit', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({username, session_cookie: session, schedule: '6h'}) }); + const data = await resp.json(); + if (data.error || data.error_message) { status.textContent = data.error || data.error_message; return; } + showToast(`Reddit: ${data.new_items} items imported`); + setTimeout(() => showConnectorsPage(), 1500); + } catch (e) { status.textContent = 'Failed: ' + e.message; } +} +window.connectConnectorReddit = connectConnectorReddit; + +// --- Linkwarden connector --- +function showConnectorLinkwardenSetup() { + _connectorInlineSetup('Connect to Linkwarden', 'Enter your Linkwarden server URL and API token', + [{id: 'url', placeholder: 'https://linkwarden.example.com'}, {id: 'token', type: 'password', placeholder: 'API token'}], + 'connectConnectorLinkwarden()'); +} +window.showConnectorLinkwardenSetup = showConnectorLinkwardenSetup; + +async function connectConnectorLinkwarden() { + const url = document.getElementById('cis-url')?.value.trim(); + const token = document.getElementById('cis-token')?.value.trim(); + const status = document.getElementById('cis-status'); + if (!url || !token) { status.textContent = 'Server URL and API token are required.'; return; } + status.textContent = 'Connecting...'; + try { + const resp = await fetch('/api/connector/linkwarden', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({base_url: url, api_token: token, schedule: '6h'}) }); + const data = await resp.json(); + if (data.error || data.error_message) { status.textContent = data.error || data.error_message; return; } + showToast(`Linkwarden: ${data.new_items} items imported`); + setTimeout(() => showConnectorsPage(), 1500); + } catch (e) { status.textContent = 'Failed: ' + e.message; } +} +window.connectConnectorLinkwarden = connectConnectorLinkwarden; + +// --- Linkding connector --- +function showConnectorLinkdingSetup() { + _connectorInlineSetup('Connect to Linkding', 'Enter your Linkding server URL and API token (Settings → API Access)', + [{id: 'url', placeholder: 'https://linkding.example.com'}, {id: 'token', type: 'password', placeholder: 'API token'}], + 'connectConnectorLinkding()'); +} +window.showConnectorLinkdingSetup = showConnectorLinkdingSetup; + +async function connectConnectorLinkding() { + const url = document.getElementById('cis-url')?.value.trim(); + const token = document.getElementById('cis-token')?.value.trim(); + const status = document.getElementById('cis-status'); + if (!url || !token) { status.textContent = 'Server URL and API token are required.'; return; } + status.textContent = 'Connecting...'; + try { + const resp = await fetch('/api/connector/linkding', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({base_url: url, api_token: token, schedule: '6h'}) }); + const data = await resp.json(); + if (data.error || data.error_message) { status.textContent = data.error || data.error_message; return; } + showToast(`Linkding: ${data.new_items} items imported`); + setTimeout(() => showConnectorsPage(), 1500); + } catch (e) { status.textContent = 'Failed: ' + e.message; } +} +window.connectConnectorLinkding = connectConnectorLinkding; + +// --- Shiori connector --- +function showConnectorShioriSetup() { + _connectorInlineSetup('Connect to Shiori', 'Enter your Shiori server URL, username, and password', + [{id: 'url', placeholder: 'https://shiori.example.com'}, {id: 'username', placeholder: 'username'}, {id: 'password', type: 'password', placeholder: 'password'}], + 'connectConnectorShiori()'); +} +window.showConnectorShioriSetup = showConnectorShioriSetup; + +async function connectConnectorShiori() { + const url = document.getElementById('cis-url')?.value.trim(); + const username = document.getElementById('cis-username')?.value.trim(); + const password = document.getElementById('cis-password')?.value.trim(); + const status = document.getElementById('cis-status'); + if (!url) { status.textContent = 'Server URL is required.'; return; } + if (!username || !password) { status.textContent = 'Username and password are required.'; return; } + status.textContent = 'Connecting...'; + try { + const resp = await fetch('/api/connector/shiori', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({base_url: url, username, password, schedule: '6h'}) }); + const data = await resp.json(); + if (data.error || data.error_message) { status.textContent = data.error || data.error_message; return; } + showToast(`Shiori: ${data.new_items} items imported`); + setTimeout(() => showConnectorsPage(), 1500); + } catch (e) { status.textContent = 'Failed: ' + e.message; } +} +window.connectConnectorShiori = connectConnectorShiori; + +// --- Wallabag connector --- +function showConnectorWallabagSetup() { + _connectorInlineSetup('Connect to Wallabag', 'Enter your Wallabag server URL and OAuth2 credentials (Settings → API Clients Management)', + [ + {id: 'url', placeholder: 'https://wallabag.example.com'}, + {id: 'client-id', placeholder: 'Client ID'}, + {id: 'client-secret', type: 'password', placeholder: 'Client secret'}, + {id: 'username', placeholder: 'Username'}, + {id: 'password', type: 'password', placeholder: 'Password'}, + ], + 'connectConnectorWallabag()'); +} +window.showConnectorWallabagSetup = showConnectorWallabagSetup; + +async function connectConnectorWallabag() { + const url = document.getElementById('cis-url')?.value.trim(); + const clientId = document.getElementById('cis-client-id')?.value.trim(); + const clientSecret = document.getElementById('cis-client-secret')?.value.trim(); + const username = document.getElementById('cis-username')?.value.trim(); + const password = document.getElementById('cis-password')?.value.trim(); + const status = document.getElementById('cis-status'); + if (!url) { status.textContent = 'Server URL is required.'; return; } + if (!clientId || !clientSecret || !username || !password) { status.textContent = 'All OAuth2 credentials are required.'; return; } + status.textContent = 'Connecting...'; + try { + const resp = await fetch('/api/connector/wallabag', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({base_url: url, client_id: clientId, client_secret: clientSecret, username, password, schedule: '6h'}) }); + const data = await resp.json(); + if (data.error || data.error_message) { status.textContent = data.error || data.error_message; return; } + showToast(`Wallabag: ${data.new_items} items imported`); + setTimeout(() => showConnectorsPage(), 1500); + } catch (e) { status.textContent = 'Failed: ' + e.message; } +} +window.connectConnectorWallabag = connectConnectorWallabag; + +// --- Configure form for active connectors --- +function _connectorConfigPanel(title, hint, fields, connectFn) { + const container = document.getElementById('connector-configure-panel'); + if (!container) return; + // Toggle off if same form is already open + if (container._currentTitle === title && container.style.display !== 'none') { + container.style.display = 'none'; + container._currentTitle = null; + return; + } + container._currentTitle = title; + const inputs = fields.map(f => ``).join(''); + container.style.display = 'block'; + container.innerHTML = ` +
+

${title}

+

${hint}

+ ${inputs} +
+ + +
+
+
`; +} + +function showConnectorConfigureForm(connectorType) { + if (connectorType === 'karakeep_api') { + _connectorConfigPanel('Reconfigure Karakeep', 'Update your Karakeep server URL and API key', + [{id: 'url', placeholder: 'https://karakeep.example.com'}, {id: 'key', type: 'password', placeholder: 'API key'}], + 'configureConnectorKarakeep()'); + } else if (connectorType === 'readwise_reader') { + _connectorConfigPanel('Reconfigure Readwise Reader', 'Update your access token (from readwise.io/access_token)', + [{id: 'token', type: 'password', placeholder: 'Access token'}], + 'configureConnectorReadwise()'); + } else if (connectorType === 'raindrop_api') { + _connectorConfigPanel('Reconfigure Raindrop.io', 'Update your test token (from app.raindrop.io/settings/integrations)', + [{id: 'token', type: 'password', placeholder: 'Test token'}], + 'configureConnectorRaindrop()'); + } else if (connectorType === 'hackernews') { + _connectorConfigPanel('Reconfigure Hacker News', 'Update your HN username', + [{id: 'username', placeholder: 'your_hn_username'}], + 'configureConnectorHN()'); + } else if (connectorType === 'reddit_saved') { + _connectorConfigPanel('Reconfigure Reddit', 'Update your Reddit username and session cookie', + [{id: 'username', placeholder: 'your_reddit_username'}, {id: 'session', type: 'password', placeholder: 'reddit_session cookie value'}], + 'configureConnectorReddit()'); + } else if (connectorType === 'linkwarden_api') { + _connectorConfigPanel('Reconfigure Linkwarden', 'Update your Linkwarden server URL and API token', + [{id: 'url', placeholder: 'https://linkwarden.example.com'}, {id: 'token', type: 'password', placeholder: 'API token'}], + 'configureConnectorLinkwarden()'); + } else if (connectorType === 'linkding_api') { + _connectorConfigPanel('Reconfigure Linkding', 'Update your Linkding server URL and API token', + [{id: 'url', placeholder: 'https://linkding.example.com'}, {id: 'token', type: 'password', placeholder: 'API token'}], + 'configureConnectorLinkding()'); + } else if (connectorType === 'shiori_api') { + _connectorConfigPanel('Reconfigure Shiori', 'Update your Shiori server URL and credentials', + [{id: 'url', placeholder: 'https://shiori.example.com'}, {id: 'username', placeholder: 'username'}, {id: 'password', type: 'password', placeholder: 'password'}], + 'configureConnectorShiori()'); + } else if (connectorType === 'wallabag_api') { + _connectorConfigPanel('Reconfigure Wallabag', 'Update your Wallabag server URL and OAuth2 credentials', + [ + {id: 'url', placeholder: 'https://wallabag.example.com'}, + {id: 'client-id', placeholder: 'Client ID'}, + {id: 'client-secret', type: 'password', placeholder: 'Client secret'}, + {id: 'username', placeholder: 'Username'}, + {id: 'password', type: 'password', placeholder: 'Password'}, + ], + 'configureConnectorWallabag()'); + } +} +window.showConnectorConfigureForm = showConnectorConfigureForm; + +async function configureConnectorKarakeep() { + const url = document.getElementById('ccp-url')?.value.trim(); + const key = document.getElementById('ccp-key')?.value.trim(); + const status = document.getElementById('ccp-status'); + if (!url || !key) { status.textContent = 'Both URL and API key are required.'; return; } + status.textContent = 'Updating...'; + try { + const resp = await fetch('/api/connector/karakeep', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({base_url: url, api_key: key, schedule: '6h'}) }); + const data = await resp.json(); + if (data.error || data.error_message) { status.textContent = data.error || data.error_message; return; } + showToast(`Karakeep: updated, ${data.new_items} new items`); + setTimeout(() => showConnectorsPage(), 1500); + } catch (e) { status.textContent = 'Failed: ' + e.message; } +} +window.configureConnectorKarakeep = configureConnectorKarakeep; + +async function configureConnectorReadwise() { + const token = document.getElementById('ccp-token')?.value.trim(); + const status = document.getElementById('ccp-status'); + if (!token) { status.textContent = 'Access token is required.'; return; } + status.textContent = 'Updating...'; + try { + const resp = await fetch('/api/connector/readwise', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({access_token: token, schedule: '6h'}) }); + const data = await resp.json(); + if (data.error || data.error_message) { status.textContent = data.error || data.error_message; return; } + showToast(`Readwise: updated, ${data.new_items} new items`); + setTimeout(() => showConnectorsPage(), 1500); + } catch (e) { status.textContent = 'Failed: ' + e.message; } +} +window.configureConnectorReadwise = configureConnectorReadwise; + +async function configureConnectorRaindrop() { + const token = document.getElementById('ccp-token')?.value.trim(); + const status = document.getElementById('ccp-status'); + if (!token) { status.textContent = 'Token is required.'; return; } + status.textContent = 'Updating...'; + try { + const resp = await fetch('/api/connector/raindrop', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({api_token: token, schedule: '6h'}) }); + const data = await resp.json(); + if (data.error || data.error_message) { status.textContent = data.error || data.error_message; return; } + showToast(`Raindrop: updated, ${data.total_found} bookmarks`); + setTimeout(() => showConnectorsPage(), 1500); + } catch (e) { status.textContent = 'Failed: ' + e.message; } +} +window.configureConnectorRaindrop = configureConnectorRaindrop; + +async function configureConnectorHN() { + const username = document.getElementById('ccp-username')?.value.trim(); + const status = document.getElementById('ccp-status'); + if (!username) { status.textContent = 'Username is required.'; return; } + status.textContent = 'Updating...'; + try { + const resp = await fetch('/api/connector/hackernews', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({username, mode: 'favorites', schedule: '1d'}) }); + const data = await resp.json(); + if (data.error || data.error_message) { status.textContent = data.error || data.error_message; return; } + showToast(`Hacker News: updated, ${data.new_items} new items`); + setTimeout(() => showConnectorsPage(), 1500); + } catch (e) { status.textContent = 'Failed: ' + e.message; } +} +window.configureConnectorHN = configureConnectorHN; + +async function configureConnectorReddit() { + const username = document.getElementById('ccp-username')?.value.trim(); + const session = document.getElementById('ccp-session')?.value.trim(); + const status = document.getElementById('ccp-status'); + if (!username || !session) { status.textContent = 'Username and session cookie are required.'; return; } + status.textContent = 'Updating...'; + try { + const resp = await fetch('/api/connector/reddit', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({username, session_cookie: session, schedule: '6h'}) }); + const data = await resp.json(); + if (data.error || data.error_message) { status.textContent = data.error || data.error_message; return; } + showToast(`Reddit: updated, ${data.new_items} new items`); + setTimeout(() => showConnectorsPage(), 1500); + } catch (e) { status.textContent = 'Failed: ' + e.message; } +} +window.configureConnectorReddit = configureConnectorReddit; + +async function configureConnectorLinkwarden() { + const url = document.getElementById('ccp-url')?.value.trim(); + const token = document.getElementById('ccp-token')?.value.trim(); + const status = document.getElementById('ccp-status'); + if (!url || !token) { status.textContent = 'Server URL and API token are required.'; return; } + status.textContent = 'Updating...'; + try { + const resp = await fetch('/api/connector/linkwarden', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({base_url: url, api_token: token, schedule: '6h'}) }); + const data = await resp.json(); + if (data.error || data.error_message) { status.textContent = data.error || data.error_message; return; } + showToast(`Linkwarden: updated, ${data.new_items} new items`); + setTimeout(() => showConnectorsPage(), 1500); + } catch (e) { status.textContent = 'Failed: ' + e.message; } +} +window.configureConnectorLinkwarden = configureConnectorLinkwarden; + +async function configureConnectorLinkding() { + const url = document.getElementById('ccp-url')?.value.trim(); + const token = document.getElementById('ccp-token')?.value.trim(); + const status = document.getElementById('ccp-status'); + if (!url || !token) { status.textContent = 'Server URL and API token are required.'; return; } + status.textContent = 'Updating...'; + try { + const resp = await fetch('/api/connector/linkding', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({base_url: url, api_token: token, schedule: '6h'}) }); + const data = await resp.json(); + if (data.error || data.error_message) { status.textContent = data.error || data.error_message; return; } + showToast(`Linkding: updated, ${data.new_items} new items`); + setTimeout(() => showConnectorsPage(), 1500); + } catch (e) { status.textContent = 'Failed: ' + e.message; } +} +window.configureConnectorLinkding = configureConnectorLinkding; + +async function configureConnectorShiori() { + const url = document.getElementById('ccp-url')?.value.trim(); + const username = document.getElementById('ccp-username')?.value.trim(); + const password = document.getElementById('ccp-password')?.value.trim(); + const status = document.getElementById('ccp-status'); + if (!url || !username || !password) { status.textContent = 'Server URL, username, and password are required.'; return; } + status.textContent = 'Updating...'; + try { + const resp = await fetch('/api/connector/shiori', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({base_url: url, username, password, schedule: '6h'}) }); + const data = await resp.json(); + if (data.error || data.error_message) { status.textContent = data.error || data.error_message; return; } + showToast(`Shiori: updated, ${data.new_items} new items`); + setTimeout(() => showConnectorsPage(), 1500); + } catch (e) { status.textContent = 'Failed: ' + e.message; } +} +window.configureConnectorShiori = configureConnectorShiori; + +async function configureConnectorWallabag() { + const url = document.getElementById('ccp-url')?.value.trim(); + const clientId = document.getElementById('ccp-client-id')?.value.trim(); + const clientSecret = document.getElementById('ccp-client-secret')?.value.trim(); + const username = document.getElementById('ccp-username')?.value.trim(); + const password = document.getElementById('ccp-password')?.value.trim(); + const status = document.getElementById('ccp-status'); + if (!url || !clientId || !clientSecret || !username || !password) { status.textContent = 'All fields are required.'; return; } + status.textContent = 'Updating...'; + try { + const resp = await fetch('/api/connector/wallabag', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({base_url: url, client_id: clientId, client_secret: clientSecret, username, password, schedule: '6h'}) }); + const data = await resp.json(); + if (data.error || data.error_message) { status.textContent = data.error || data.error_message; return; } + showToast(`Wallabag: updated, ${data.new_items} new items`); + setTimeout(() => showConnectorsPage(), 1500); + } catch (e) { status.textContent = 'Failed: ' + e.message; } +} +window.configureConnectorWallabag = configureConnectorWallabag; + function showAddFeedForm() { const container = document.getElementById('add-feed-form'); if (!container) return; @@ -4812,49 +5197,6 @@ function renderSettings(data) { settingsField('github.token', 'GitHub Token', s.github?.token, 'password', ''), ], ``); - // Connectors - html += settingsSection('Karakeep', [ - settingsField('karakeep.url', 'Server URL', s.karakeep?.url, 'text', 'https://karakeep.example.com'), - settingsField('karakeep.api_key', 'API Key', s.karakeep?.api_key, 'password', ''), - ], ``); - - html += settingsSection('Readwise Reader', [ - settingsField('readwise.token', 'Access Token', s.readwise?.token, 'password', ''), - ], ``); - - html += settingsSection('Hacker News', [ - settingsField('hackernews.username', 'Username', s.hackernews?.username, 'text', 'your_hn_username'), - ], ``); - - html += settingsSection('Reddit', [ - settingsField('reddit.username', 'Username', s.reddit?.username, 'text', 'your_reddit_username'), - settingsField('reddit.session_cookie', 'Session Cookie', s.reddit?.session_cookie, 'password', 'reddit_session value'), - ], ``); - - html += settingsSection('Linkwarden', [ - settingsField('linkwarden.url', 'Server URL', s.linkwarden?.url, 'text', 'https://linkwarden.example.com'), - settingsField('linkwarden.api_key', 'API Key', s.linkwarden?.api_key, 'password', ''), - ], ``); - - html += settingsSection('Linkding', [ - settingsField('linkding.url', 'Server URL', s.linkding?.url, 'text', 'https://linkding.example.com'), - settingsField('linkding.api_key', 'API Key', s.linkding?.api_key, 'password', ''), - ], ``); - - html += settingsSection('Shiori', [ - settingsField('shiori.url', 'Server URL', s.shiori?.url, 'text', 'https://shiori.example.com'), - settingsField('shiori.username', 'Username', s.shiori?.username, 'text', ''), - settingsField('shiori.password', 'Password', s.shiori?.password, 'password', ''), - ], ``); - - html += settingsSection('Wallabag', [ - settingsField('wallabag.url', 'Server URL', s.wallabag?.url, 'text', 'https://wallabag.example.com'), - settingsField('wallabag.client_id', 'Client ID', s.wallabag?.client_id, 'text', ''), - settingsField('wallabag.client_secret', 'Client Secret', s.wallabag?.client_secret, 'password', ''), - settingsField('wallabag.username', 'Username', s.wallabag?.username, 'text', ''), - settingsField('wallabag.password', 'Password', s.wallabag?.password, 'password', ''), - ], ``); - // About const pythonVer = data.python_version || ''; const embedModel = data.embedding_model || ''; @@ -5154,22 +5496,6 @@ async function testConnection(provider) { const v = (id) => document.getElementById(`settings-${id}`)?.value || ''; if (provider === 'llm') { body = { provider: 'llm', base_url: v('llm.base_url') }; - } else if (provider === 'karakeep') { - body = { provider: 'karakeep', base_url: v('karakeep.url'), api_key: v('karakeep.api_key') }; - } else if (provider === 'readwise') { - body = { provider: 'readwise', token: v('readwise.token') }; - } else if (provider === 'hackernews') { - body = { provider: 'hackernews', username: v('hackernews.username') }; - } else if (provider === 'reddit') { - body = { provider: 'reddit', username: v('reddit.username'), session_cookie: v('reddit.session_cookie') }; - } else if (provider === 'linkwarden') { - body = { provider: 'linkwarden', base_url: v('linkwarden.url'), api_key: v('linkwarden.api_key') }; - } else if (provider === 'linkding') { - body = { provider: 'linkding', base_url: v('linkding.url'), api_key: v('linkding.api_key') }; - } else if (provider === 'shiori') { - body = { provider: 'shiori', base_url: v('shiori.url'), username: v('shiori.username'), password: v('shiori.password') }; - } else if (provider === 'wallabag') { - body = { provider: 'wallabag', base_url: v('wallabag.url'), client_id: v('wallabag.client_id'), client_secret: v('wallabag.client_secret'), username: v('wallabag.username'), password: v('wallabag.password') }; } else { const embedType = document.getElementById('settings-embedding.provider')?.value || 'ollama'; body = { provider: 'embedding', type: embedType, api_url: v('embedding.api_url') };