import React, { useState, useEffect } from 'react' import { api } from './api' export default function Integrations() { const [integrations, setIntegrations] = useState([]) const [events, setEvents] = useState(null) const [userId] = useState(1) const [msg, setMsg] = useState(null) const [loadingId, setLoadingId] = useState(null) const [loading, setLoading] = useState(false) const [error, setError] = useState(null) const [adminSettings, setAdminSettings] = useState(null) const [providerCaps, setProviderCaps] = useState(null) const [details, setDetails] = useState({}) const [hooksSchema, setHooksSchema] = useState(null) const [hooksExample, setHooksExample] = useState(null) const [orchestration, setOrchestration] = useState(null) const [autoRefresh, setAutoRefresh] = useState(false) const [refreshIntervalSec, setRefreshIntervalSec] = useState(10) const [sortKey, setSortKey] = useState('provider') const [sortDir, setSortDir] = useState('asc') const [orchLoading, setOrchLoading] = useState(false) useEffect(() => { setLoading(true); setError(null) api(`/v1/users/${userId}/integrations`).then(d => { setIntegrations(d) // fetch details for last sync display d.forEach(i => { api(`/v1/integrations/${i.id}`).then(info => { setDetails(prev => ({ ...prev, [i.id]: info })) }).catch(() => { }) }) }).catch((e) => { setError(String(e)); setIntegrations([]) }).finally(() => setLoading(false)) // load admin settings if available api('/v1/admin/settings').then(setAdminSettings).catch(() => { }) api('/v1/admin/provider_caps').then(setProviderCaps).catch(() => { }) api('/v1/admin/hooks/schema').then((d) => { setHooksSchema(d.schema || null) try { const ex = Array.isArray(d.examples) && d.examples.length ? d.examples[0] : null setHooksExample(ex && ex.hooks ? ex.hooks : null) } catch (_) { /* noop */ } }).catch(() => { }) setOrchLoading(true) api('/v1/admin/orchestration').then(setOrchestration).catch(() => { }).finally(() => setOrchLoading(false)) }, [userId]) useEffect(() => { if (!autoRefresh) return const ms = Math.max(3, parseInt(String(refreshIntervalSec || 10), 10)) * 1000 const id = setInterval(() => { setOrchLoading(true) api('/v1/admin/orchestration').then(setOrchestration).catch(() => { }).finally(() => setOrchLoading(false)) }, ms) return () => clearInterval(id) }, [autoRefresh, refreshIntervalSec]) function refreshOrchestration() { setOrchLoading(true) api('/v1/admin/orchestration').then(setOrchestration).catch(() => { }).finally(() => setOrchLoading(false)) } function toggleSort(key) { if (sortKey === key) { setSortDir(sortDir === 'asc' ? 'desc' : 'asc') } else { setSortKey(key) setSortDir('asc') } } function startGoogle() { // Open backend OAuth URL in new window so the redirect can complete window.open(`/api/v1/oauth/google/login?user_id=${userId}`, '_blank') } function fetchEvents(integrationId) { setLoadingId(integrationId) api(`/v1/integrations/${integrationId}/google/events`) .then(d => { setEvents(d) setMsg('Fetched events') }) .catch(e => { setEvents({ error: String(e) }); setMsg('Fetch failed') }) .finally(() => setLoadingId(null)) } function previewEvents(integrationId) { api(`/v1/integrations/${integrationId}/events_preview`).then(d => { setEvents(d) setMsg('Preview loaded') }).catch(() => setMsg('Preview failed')) } function removeIntegration(integrationId) { if (!confirm('Remove integration?')) return setLoadingId(integrationId) api(`/v1/integrations/${integrationId}`, { method: 'DELETE' }) .then(d => { setMsg('Integration removed') setIntegrations(integrations.filter(i => i.id !== integrationId)) }) .catch(e => setMsg('Failed to remove')) .finally(() => setLoadingId(null)) } function syncIntegration(integrationId) { setLoadingId(integrationId) api(`/v1/integrations/${integrationId}/sync_to_habits`, { method: 'POST' }) .then(d => setMsg(`Synced ${d.count || 0} items`)) .catch(e => setMsg('Sync failed')) .finally(() => setLoadingId(null)) } function setIntegrationConfig(id, patch) { // naive: fetch current integration then patch config server-side via a simple endpoint api(`/v1/integrations/${id}`).then(cur => { const cfg = { ...(cur.config ? JSON.parse(cur.config) : {}), ...patch } api(`/v1/integrations/${id}`, { method: 'PATCH', body: { config: cfg } }) .then(() => setMsg('Settings updated')) .catch(() => setMsg('Failed to update settings')) }).catch(() => setMsg('Failed to load integration')) } return (

Integrations

{adminSettings && (
Admin Settings
Default sync interval (s): {adminSettings.default_sync_interval_seconds}
{providerCaps && (
Provider concurrency caps (default: {providerCaps.default})
{Object.keys(providerCaps.caps || {}).map(p => (
{ const v = parseInt(e.target.value || '0', 10) const caps = { ...(providerCaps.caps || {}), [p]: v } api('/v1/admin/provider_caps', { method: 'POST', body: { caps } }) .then(() => setProviderCaps({ ...providerCaps, caps })) .catch(() => setMsg('Failed to update caps')) }} style={{ width: 80 }} />
))}
)} {orchestration && (
Orchestration
{orchLoading && Refreshing…}
{(() => { const rows = [...(orchestration.providers || [])] const toVal = (p, k) => { if (k === 'provider') return (p.provider || (p.queue ? `RQ ${p.queue}` : '') || '').toLowerCase() if (k === 'inflight') return Number.isFinite(p.inflight) ? p.inflight : -1 if (k === 'queue') return Number.isFinite(p.queue_depth) ? p.queue_depth : (Number.isFinite(p.rq_length) ? p.rq_length : -1) if (k === 'cap') return Number.isFinite(p.cap) ? p.cap : -1 return 0 } rows.sort((a, b) => { const av = toVal(a, sortKey) const bv = toVal(b, sortKey) if (av < bv) return sortDir === 'asc' ? -1 : 1 if (av > bv) return sortDir === 'asc' ? 1 : -1 return 0 }) return rows.map((p, idx) => { const cap = Number.isFinite(p.cap) ? p.cap : null const inflight = Number.isFinite(p.inflight) ? p.inflight : null let badge = null if (cap && inflight !== null && cap > 0) { const util = Math.round((inflight / cap) * 100) let bg = '#e6f4ea', color = '#1e4620' if (util >= 100) { bg = '#fdecea'; color = '#b71c1c' } else if (util >= 80) { bg = '#fff4e5'; color = '#8a4500' } badge = {util}% } return ( ) }) })()}
toggleSort('provider')}>Provider {sortKey === 'provider' ? (sortDir === 'asc' ? '▲' : '▼') : ''} toggleSort('inflight')}>In-flight {sortKey === 'inflight' ? (sortDir === 'asc' ? '▲' : '▼') : ''} toggleSort('queue')}>Queue Depth {sortKey === 'queue' ? (sortDir === 'asc' ? '▲' : '▼') : ''} toggleSort('cap')}>Cap {sortKey === 'cap' ? (sortDir === 'asc' ? '▲' : '▼') : ''}
{p.provider || (p.queue ? `RQ ${p.queue}` : '')} {badge} {p.inflight ?? ''} {p.queue_depth ?? (p.rq_length ?? '')} {p.cap ?? ''}
)}
)}

Your Integrations

{loading &&
Loading…
} {error &&
{error}
}