Files
simbarag/raggr-frontend/src/components/IntegrationStatusPanel.tsx
T
ryan 5809ad3adb
CI / backend (pull_request) Successful in 42s
CI / frontend (pull_request) Failing after 7s
CI / backend (push) Successful in 41s
CI / frontend (push) Failing after 7s
Add durable chat runs, shared agent runtime, reliable jobs, history tools and citations
Shared runtime
- One agent entry point (runtime.invoke_agent_for_user / prepare_agent_turn) for
  web, email, iMessage, WhatsApp, scheduled and cron, so identity, permissions,
  memories, summaries and metrics match across channels.
- Serialize turns per checkpoint thread; /query validates input and returns 409
  while a durable run owns the conversation.

Reconnectable chat (durable runs)
- POST /runs creates an idempotent, owner-scoped run; GET .../events replays an
  ordered SSE log by cursor, so generation survives refresh and device switches.
  Stop cancels; disconnect only detaches. /stream-query is a compatibility adapter.
- Coalesce streamed deltas (150 ms) and wake same-process subscribers instead of
  polling; keep buffered text when a run is stopped.
- Drain runs on shutdown; serve via Hypercorn with a shutdown timeout covering
  the drain (and no longer in debug mode). CHAT_RUN_* settings are forwarded.

Scheduled messages and cron
- Atomic delivery claims, attempt history and explicit retries; ambiguous sends
  become "uncertain" and are never resent automatically.
- Cron triggers need a run_id/Idempotency-Key; legacy jobs need an explicit owner.
- Cancelling never rewrites a started delivery; series stop via tombstones.
  Legacy sent rows are not forked into new series. Malformed bodies return 400.

History, integrations and retrieval
- Message-content search, pinned conversations, JSON/Markdown export and a
  memory editor.
- Integration status panel and YNAB data-freshness provenance.
- Date-filtered document/note search with stable citations and a source viewer.

Migration 20 is generated by aerich.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 23:11:04 -04:00

58 lines
3.3 KiB
TypeScript

import { useEffect, useState } from "react";
import { X, RefreshCw } from "lucide-react";
import { userService } from "../api/userService";
type Status = { name: string; status: string; message: string; last_success_at: string | null; indexed_chunks?: number };
export const IntegrationStatusPanel = ({ onClose }: { onClose: () => void }) => {
const [items, setItems] = useState<Status[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
const [revision, setRevision] = useState(0);
useEffect(() => {
const controller = new AbortController();
setLoading(true);
setError("");
void (async () => {
try {
const response = await userService.fetchWithAuth("/api/integrations/", { signal: controller.signal });
if (!response.ok) throw new Error("Unable to load integration status.");
const body = await response.json();
if (!controller.signal.aborted) setItems(body.integrations);
} catch (err) {
if (!controller.signal.aborted) setError(err instanceof Error ? err.message : "Unable to load integration status.");
} finally {
if (!controller.signal.aborted) setLoading(false);
}
})();
return () => controller.abort();
}, [revision]);
useEffect(() => {
const close = (event: KeyboardEvent) => { if (event.key === "Escape") onClose(); };
window.addEventListener("keydown", close);
return () => window.removeEventListener("keydown", close);
}, [onClose]);
return (
<div className="fixed inset-0 z-50 bg-charcoal/30 flex items-center justify-center p-4" onClick={onClose}>
<section role="dialog" aria-modal="true" aria-labelledby="integration-status-title" className="bg-cream rounded-2xl shadow-xl w-full max-w-xl max-h-[85vh] overflow-y-auto p-5" onClick={(e) => e.stopPropagation()}>
<header className="flex items-center justify-between gap-3 mb-4">
<h2 id="integration-status-title" className="font-semibold text-charcoal">Integrations and data freshness</h2>
<div className="flex gap-2">
<button aria-label="Refresh integration status" disabled={loading} onClick={() => setRevision((r) => r + 1)} className="p-2 rounded-lg hover:bg-cream-dark"><RefreshCw size={18} /></button>
<button aria-label="Close integration status" onClick={onClose} className="p-2 rounded-lg hover:bg-cream-dark"><X size={18} /></button>
</div>
</header>
{loading && <p role="status" className="text-sm text-warm-gray">Checking data freshness…</p>}
{error && <p role="alert" className="text-sm text-red-700">{error}</p>}
<ul className="space-y-3">
{items.map((item) => <li key={item.name} className="border border-sand-light rounded-xl p-3">
<div className="flex justify-between gap-2"><span className="font-medium">{item.name}</span><span className={item.status === "ready" ? "text-leaf-dark text-sm" : item.status === "stale" || item.status === "unavailable" ? "text-amber-800 text-sm" : "text-warm-gray text-sm"}>{item.status.replace(/_/g, " ")}</span></div>
<p className="text-sm text-warm-gray mt-1">{item.message}</p>
{item.last_success_at && <p className="text-xs text-warm-gray mt-1">Last synchronized: {new Date(item.last_success_at).toLocaleString()}</p>}
</li>)}
</ul>
</section>
</div>
);
};