5809ad3adb
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>
58 lines
3.3 KiB
TypeScript
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>
|
|
);
|
|
};
|