Chybova hlaseni konektoru rikaji, co se stalo, a kam to slo
Duvod od sluzby jde primo do hlasky: z tela odpovedi se vytahne detail, error_description, message, title, error i seznam missingHeaders. Retezec, ktery vypada jako JSON, se rozbaluje dal - sluzba iDoklad presne takhle predava telo od iDokladu samotneho. Kdyz sluzba nenapsala nic, rekne se to. 401 a 403 uz nejsou jedna hlaska. 401 = udaje sluzba dostala a neuznala je. 403 = tvar udaju je v poradku, zakazuje se samo volani. V kazde hlasce je cela adresa vcetne serveru (ScriptRequestInfo.url), bez query - v query muze byt tajemstvi. Zaklad adresy je z konfigurace a konektor ho smi prepsat, takze se neda odvodit z toho, kde je nasazeny portal. Adresa je videt i na karte konektoru a v odpovedi na test, i kdyz overeni projde. Tlacitko Logy na karte konektoru a historie poslednich peti overeni. Odpoved sluzby dosud existovala jen v odpovedi na test, tedy do prekresleni stranky, a v logu containeru. Do logu containeru se nikdo divat nechodi. Zaznam se uklada i pri uspechu, jinak by neslo poznat, jestli konektor nesel nikdy, nebo prestal jit ve chvili, kdy nekdo sahnul na udaje. Migrace 003_connector_checks.sql, endpoint GET /connectors/:id/checks. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
6297bbf480
commit
5a124a53d8
@@ -18,24 +18,31 @@ export function ErrorDetail({
|
||||
request,
|
||||
retryable,
|
||||
kind,
|
||||
defaultOpen = false,
|
||||
className,
|
||||
}: {
|
||||
message: string;
|
||||
/** Cela odpoved sluzby nebo zasobnik volani. */
|
||||
detail?: string;
|
||||
status?: number;
|
||||
request?: { method: string; path: string };
|
||||
/** `url` je cela adresa vcetne serveru. Bez ni nejde poznat, kam to slo. */
|
||||
request?: { method: string; path: string; url?: string };
|
||||
retryable?: boolean;
|
||||
kind?: string;
|
||||
/**
|
||||
* Rozbalit odpoved sluzby rovnou. U overeni konektoru ano: tam je telo
|
||||
* odpovedi to jedine, co odlisi spatne udaje od nepovolene IP adresy.
|
||||
*/
|
||||
defaultOpen?: boolean;
|
||||
className?: string;
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [open, setOpen] = useState(defaultOpen);
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
/** Co se zkopiruje: vsechno, co o chybe vime, at to jde poslat dal. */
|
||||
const fullText = [
|
||||
message,
|
||||
request ? `Volání: ${request.method} ${request.path}` : null,
|
||||
request ? `Volání: ${request.method} ${request.url ?? request.path}` : null,
|
||||
status !== undefined ? `HTTP: ${status}` : null,
|
||||
kind ? `Druh: ${kind}` : null,
|
||||
retryable !== undefined
|
||||
@@ -71,7 +78,7 @@ export function ErrorDetail({
|
||||
<p className="mt-1 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-white/40">
|
||||
{request && (
|
||||
<span className="font-mono">
|
||||
{request.method} {request.path}
|
||||
{request.method} {request.url ?? request.path}
|
||||
</span>
|
||||
)}
|
||||
{status !== undefined && <span>HTTP {status}</span>}
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
EyeOff,
|
||||
Pencil,
|
||||
Plus,
|
||||
ScrollText,
|
||||
Star,
|
||||
Trash2,
|
||||
Wifi,
|
||||
@@ -20,14 +21,17 @@ import { Button } from '@/components/ui/Button';
|
||||
import { Modal } from '@/components/ui/Modal';
|
||||
import { apiFetch, ApiError } from '@/lib/api';
|
||||
import { cn } from '@/lib/cn';
|
||||
import { formatDateTime } from '@/lib/format';
|
||||
import { serviceIcon } from '@/lib/serviceIcons';
|
||||
import { usePageMeta } from '@/lib/usePageMeta';
|
||||
import type {
|
||||
Connector,
|
||||
ConnectorCheck,
|
||||
ConnectorTestResult,
|
||||
Service,
|
||||
ServiceCredentialField,
|
||||
ServiceOverview,
|
||||
ServiceWithUsage,
|
||||
StorageStatus,
|
||||
} from '@/types/dashboard';
|
||||
|
||||
@@ -80,7 +84,8 @@ export default function Connectors() {
|
||||
useEffect(load, [load]);
|
||||
|
||||
const servicesById = useMemo(() => {
|
||||
const map = new Map<string, Service>();
|
||||
// ServiceWithUsage, ne Service: karta ukazuje i vychozi adresu sluzby.
|
||||
const map = new Map<string, ServiceWithUsage>();
|
||||
for (const service of services?.items ?? []) map.set(service.id, service);
|
||||
return map;
|
||||
}, [services]);
|
||||
@@ -207,14 +212,21 @@ function ConnectorCard({
|
||||
onChanged,
|
||||
}: {
|
||||
connector: Connector;
|
||||
service: Service | undefined;
|
||||
service: ServiceWithUsage | undefined;
|
||||
onEdit: () => void;
|
||||
onChanged: () => void;
|
||||
}) {
|
||||
/**
|
||||
* Kam konektor opravdu miri. Vychozi adresa je z konfigurace serveru
|
||||
* a konektor ji smi prepsat, takze bez ni je "vratilo 403" jen pulka
|
||||
* informace: nerika, jestli se to vubec trefilo na spravny stroj.
|
||||
*/
|
||||
const targetUrl = connector.baseUrl ?? service?.baseUrl ?? null;
|
||||
const Icon = serviceIcon(service?.icon ?? 'Plug');
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [test, setTest] = useState<ConnectorTestResult | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [logsOpen, setLogsOpen] = useState(false);
|
||||
|
||||
async function handleTest() {
|
||||
setBusy(true);
|
||||
@@ -258,6 +270,11 @@ function ConnectorCard({
|
||||
)}
|
||||
</h2>
|
||||
<p className="truncate text-xs text-white/40">{service?.name ?? connector.serviceId}</p>
|
||||
{targetUrl && (
|
||||
<p className="truncate font-mono text-xs text-white/30" title={targetUrl}>
|
||||
{targetUrl}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -279,7 +296,7 @@ function ConnectorCard({
|
||||
<p className="mt-2 text-xs text-danger-400">
|
||||
{connector.lastError}
|
||||
<span className="mt-0.5 block text-white/35">
|
||||
Celé hlášení uvidíte po stisku Ověřit.
|
||||
Celá odpověď služby je pod tlačítkem Logy.
|
||||
</span>
|
||||
</p>
|
||||
)}
|
||||
@@ -301,6 +318,7 @@ function ConnectorCard({
|
||||
detail={test.detail}
|
||||
status={test.status}
|
||||
request={test.request}
|
||||
defaultOpen
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -315,15 +333,125 @@ function ConnectorCard({
|
||||
<Wifi className="size-3.5" />
|
||||
{busy ? 'Ověřuji...' : 'Ověřit'}
|
||||
</Button>
|
||||
<Button size="sm" variant="ghost" onClick={() => setLogsOpen(true)}>
|
||||
<ScrollText className="size-3.5" />
|
||||
Logy
|
||||
{connector.checkCount > 0 && (
|
||||
<span className="text-white/40">{connector.checkCount}</span>
|
||||
)}
|
||||
</Button>
|
||||
<Button size="sm" variant="ghost" onClick={handleDelete} disabled={busy}>
|
||||
<Trash2 className="size-3.5" />
|
||||
Smazat
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<ConnectorLogs
|
||||
connector={connector}
|
||||
targetUrl={targetUrl}
|
||||
open={logsOpen}
|
||||
onClose={() => setLogsOpen(false)}
|
||||
/>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Poslednich pet overeni konektoru.
|
||||
*
|
||||
* Duvod, proc to je vlastni dialog a ne radek na karte: u chyby je podstatne
|
||||
* cele telo odpovedi sluzby, a to je nekolik radku. Do karty se to neveslo,
|
||||
* takze to dosud nebylo nikde - jen v odpovedi na test, ktera zmizela
|
||||
* s prekreslenim stranky.
|
||||
*
|
||||
* Nacita se az pri otevreni. V seznamu konektoru by to byla zatez navic
|
||||
* za neco, co vetsinu casu nikdo necte.
|
||||
*/
|
||||
function ConnectorLogs({
|
||||
connector,
|
||||
targetUrl,
|
||||
open,
|
||||
onClose,
|
||||
}: {
|
||||
connector: Connector;
|
||||
/** Kam konektor miri. V hlavicce dialogu, at je to videt bez rozklikavani. */
|
||||
targetUrl: string | null;
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const [items, setItems] = useState<ConnectorCheck[] | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
setItems(null);
|
||||
setError(null);
|
||||
apiFetch<{ items: ConnectorCheck[] }>(`/api/dashboard/connectors/${connector.id}/checks`)
|
||||
.then((response) => setItems(response.items))
|
||||
.catch((err: unknown) => {
|
||||
setError(err instanceof Error ? err.message : 'Logy se nepodařilo načíst.');
|
||||
});
|
||||
}, [open, connector.id]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
title={`Logy: ${connector.name}`}
|
||||
description="Posledních pět ověření. U neúspěchu je vidět celá odpověď služby."
|
||||
>
|
||||
{targetUrl && (
|
||||
<p className="border-b border-ink-600/60 px-5 py-3 font-mono text-xs break-all text-white/40">
|
||||
{targetUrl}
|
||||
</p>
|
||||
)}
|
||||
<div className="max-h-[70vh] space-y-3 overflow-y-auto p-5">
|
||||
{error && <p className="text-sm text-danger-400">{error}</p>}
|
||||
{!error && items === null && <p className="text-sm text-white/40">Načítám...</p>}
|
||||
{items !== null && items.length === 0 && (
|
||||
<p className="text-sm text-white/40">
|
||||
Zatím tu nic není. Záznam vznikne při prvním stisku Ověřit.
|
||||
</p>
|
||||
)}
|
||||
|
||||
{items?.map((check, index) => (
|
||||
<div key={`${check.at}-${index}`} className="rounded-lg border border-ink-600/60 p-3">
|
||||
<div className="flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-white/40">
|
||||
<span className="text-white/70">{formatDateTime(check.at)}</span>
|
||||
{check.ok ? <Badge tone="ok">prošlo</Badge> : <Badge tone="danger">selhalo</Badge>}
|
||||
<span>Ověřeno: {check.checked}</span>
|
||||
</div>
|
||||
|
||||
{check.ok ? (
|
||||
<p className="mt-2 flex items-start gap-1.5 text-sm text-ok-400">
|
||||
<CheckCircle2 className="mt-0.5 size-3.5 shrink-0" />
|
||||
<span>
|
||||
{check.message}
|
||||
{check.request && (
|
||||
<span className="mt-0.5 block font-mono text-xs break-all text-white/40">
|
||||
{check.request.method} {check.request.url}
|
||||
{check.status !== null && ` -> HTTP ${check.status}`}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</p>
|
||||
) : (
|
||||
<ErrorDetail
|
||||
className="mt-2"
|
||||
message={check.message}
|
||||
detail={check.detail ?? undefined}
|
||||
status={check.status ?? undefined}
|
||||
request={check.request ?? undefined}
|
||||
defaultOpen
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
function StatusBadge({ connector }: { connector: Connector }) {
|
||||
if (!connector.ready) return <Badge tone="warn">Chybí údaje</Badge>;
|
||||
if (connector.status === 'ok') return <Badge tone="ok">Ověřeno</Badge>;
|
||||
|
||||
@@ -353,6 +353,8 @@ export interface Connector {
|
||||
status: 'untested' | 'ok' | 'error';
|
||||
lastCheckAt: string | null;
|
||||
lastError: string | null;
|
||||
/** Kolik zaznamu o overeni je v historii. Samotna historie se cte zvlast. */
|
||||
checkCount: number;
|
||||
isDefault: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
@@ -380,16 +382,33 @@ export interface StorageStatus {
|
||||
location: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Jeden zaznam v historii overeni konektoru.
|
||||
* Nese cele telo odpovedi sluzby, protoze prave tam je napsane, co ji vadilo.
|
||||
*/
|
||||
export interface ConnectorCheck {
|
||||
at: string;
|
||||
ok: boolean;
|
||||
checked: string;
|
||||
status: number | null;
|
||||
message: string;
|
||||
detail: string | null;
|
||||
/** Cela adresa vcetne serveru, bez query. */
|
||||
request: { method: string; path: string; url: string } | null;
|
||||
}
|
||||
|
||||
export interface ConnectorTestResult {
|
||||
ok: boolean;
|
||||
/** Co se vlastne overilo: pristupove udaje, nebo jen dostupnost sluzby. */
|
||||
checked: string;
|
||||
status?: number;
|
||||
message: string;
|
||||
/** Ktere volani to bylo. Bez toho je zprava jen pulka informace. */
|
||||
request?: { method: string; path: string };
|
||||
/** Ktere volani to bylo, vcetne serveru. Bez toho je zprava jen pulka informace. */
|
||||
request?: { method: string; path: string; url: string };
|
||||
/** Cela odpoved sluzby. Tady je napsane, co ji vadilo. */
|
||||
detail?: string;
|
||||
/** Zaklad adresy, na kterou konektor miri. Vraci se i kdyz overeni projde. */
|
||||
baseUrl?: string;
|
||||
}
|
||||
|
||||
export interface ServiceCatalog {
|
||||
@@ -743,7 +762,7 @@ export interface ScriptRunResult {
|
||||
status?: number;
|
||||
/** Cela odpoved sluzby, nebo zasobnik volani u chyby ve skriptu. */
|
||||
detail?: string;
|
||||
request?: { method: string; path: string };
|
||||
request?: { method: string; path: string; url?: string };
|
||||
/** Jen mimo produkci. */
|
||||
stack?: string;
|
||||
issues?: Array<{ field: string; message: string }>;
|
||||
|
||||
Reference in New Issue
Block a user