Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions gui/src/components/protocols/ProtocolBadge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { parseProtocolTraceV1 } from "../../../../src/protocols/dto";
import type { TFn } from "../../i18n/shared";
import { PROTOCOL_MODE_KEYS, protocolCompactLabel } from "./protocol-labels";

/**
* Compact protocol path for a Logs row, e.g. "Chat → Chat · Native". The mode is written out,
* never conveyed by colour alone. A row without a valid trace renders nothing: an old row is
* not guessed at.
*/
export function ProtocolBadge({ trace, t }: { trace: unknown; t: TFn }) {
const parsed = parseProtocolTraceV1(trace);
if (!parsed) return null;
const path = protocolCompactLabel(parsed, t);
const mode = t(PROTOCOL_MODE_KEYS[parsed.mode]);
return (
<span
className="badge badge-muted"
style={{ whiteSpace: "nowrap" }}
title={t("logs.protocol.badgeTitle", { path, mode })}
data-protocol-mode={parsed.mode}
>
{path} · {mode}
</span>
);
}
69 changes: 69 additions & 0 deletions gui/src/components/protocols/ProtocolTracePanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { parseProtocolTraceV1 } from "../../../../src/protocols/dto";
import type { TFn } from "../../i18n/shared";
import { FEATURE_DISPOSITION_KEYS, PROTOCOL_MODE_KEYS, protocolHopLabel, protocolPathLabel } from "./protocol-labels";

/**
* The protocol section of the Logs detail dialog: what this request actually did, attempt by
* attempt. Reason codes and feature ids are fixed machine identifiers and render as code; the
* internal Responses hop is labelled as internal. A row without a valid trace says so instead
* of inferring a path from other fields.
*/
export function ProtocolTracePanel({ trace, t }: { trace: unknown; t: TFn }) {
const parsed = parseProtocolTraceV1(trace);
return (
<section className="log-detail-section" aria-labelledby="log-detail-protocol">
<h4 id="log-detail-protocol" className="log-detail-section-title">{t("logs.detail.protocol.section")}</h4>
{parsed ? (
<div className="log-detail-grid" data-protocol-mode={parsed.mode}>
<span className="muted">{t("logs.detail.protocol.inbound")}</span>
<span>{protocolHopLabel(parsed.inbound, t)}</span>
<span className="muted">{t("logs.detail.protocol.mode")}</span>
<span>{t(PROTOCOL_MODE_KEYS[parsed.mode])}</span>
{parsed.upstream && (
<>
<span className="muted">{t("logs.detail.protocol.upstream")}</span>
<span>{protocolHopLabel(parsed.upstream, t)}</span>
</>
)}
{parsed.requestPath.length > 0 && (
<>
<span className="muted">{t("logs.detail.protocol.requestPath")}</span>
<span>{protocolPathLabel(parsed.requestPath, t)}</span>
<span className="muted">{t("logs.detail.protocol.responsePath")}</span>
<span>{protocolPathLabel(parsed.responsePath, t)}</span>
</>
)}
<span className="muted">{t("logs.detail.protocol.reasons")}</span>
<span className="mono log-detail-break">{parsed.reasonCodes.join(", ") || "–"}</span>
{parsed.featureEffects && parsed.featureEffects.length > 0 && (
<>
<span className="muted">{t("logs.detail.protocol.features")}</span>
<span>
{parsed.featureEffects.map(effect => (
<span key={effect.feature} style={{ display: "block" }}>
<code>{effect.feature}</code>: {t(FEATURE_DISPOSITION_KEYS[effect.disposition])}
</span>
))}
</span>
</>
)}
{parsed.attempts && parsed.attempts.length > 0 && (
<>
<span className="muted">{t("logs.detail.protocol.attempts")}</span>
<span>
{parsed.attempts.map(attempt => (
<span key={attempt.ordinal} style={{ display: "block" }}>
{t("logs.detail.protocol.attempt", { ordinal: attempt.ordinal })}:{" "}
{protocolPathLabel(attempt.requestPath, t)} · {t(PROTOCOL_MODE_KEYS[attempt.mode])}
</span>
))}
</span>
</>
)}
</div>
) : (
<p className="log-detail-notes-line muted">{t("logs.detail.protocol.none")}</p>
)}
</section>
);
}
53 changes: 53 additions & 0 deletions gui/src/components/protocols/protocol-labels.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import type { DeliveryMode, ProtocolHop } from "../../../../src/protocols/contract";
import type { ProtocolTraceV1 } from "../../../../src/protocols/dto";
import type { FeatureDisposition } from "../../../../src/protocols/features";
import type { TFn, TKey } from "../../i18n/shared";

/**
* Localized labels for the closed protocol vocabulary. The records are exhaustive over the
* contract's unions, so a new mode, hop or disposition fails the dashboard typecheck instead of
* rendering a raw identifier.
*/
export const PROTOCOL_MODE_KEYS: Record<DeliveryMode, TKey> = {
native: "logs.protocol.mode.native",
translated: "logs.protocol.mode.translated",
"legacy-bridge": "logs.protocol.mode.legacyBridge",
blocked: "logs.protocol.mode.blocked",
};

export const FEATURE_DISPOSITION_KEYS: Record<FeatureDisposition, TKey> = {
passthrough: "logs.protocol.disposition.passthrough",
translated: "logs.protocol.disposition.translated",
degraded: "logs.protocol.disposition.degraded",
unsupported: "logs.protocol.disposition.unsupported",
};

const HOP_KEYS: Record<ProtocolHop, TKey> = {
responses: "logs.protocol.wire.responses",
chat: "logs.protocol.wire.chat",
messages: "logs.protocol.wire.messages",
other: "logs.protocol.wire.other",
ir: "logs.protocol.hop.ir",
"responses-internal": "logs.protocol.hop.internal",
};

const ARROW = " → ";

export function protocolHopLabel(hop: ProtocolHop, t: TFn): string {
return t(HOP_KEYS[hop]);
}

export function protocolPathLabel(path: readonly ProtocolHop[], t: TFn): string {
return path.map(hop => protocolHopLabel(hop, t)).join(ARROW);
}

/**
* The compact list label: the client wire and the upstream wire, without the intermediate
* hops the detail panel spells out. A refusal has no upstream, so it shows the client wire only.
*/
export function protocolCompactLabel(trace: ProtocolTraceV1, t: TFn): string {
const inbound = protocolHopLabel(trace.inbound, t);
if (trace.mode === "blocked") return inbound;
const upstream = trace.upstream ?? trace.requestPath[trace.requestPath.length - 1];
return upstream ? `${inbound}${ARROW}${protocolHopLabel(upstream, t)}` : inbound;
}
29 changes: 29 additions & 0 deletions gui/src/i18n/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -877,6 +877,35 @@ export const de: Record<TKey, string> = {
"logs.detail.route.selected": "Ausgewählt",
"logs.detail.route.candidates": "Kandidaten",
"logs.detail.route.unknown": "Für diese Anfrage wurde keine Route-Entscheidung aufgezeichnet (Zeile vor dem Trace).",
"logs.filter.protocol.label": "Protokollpfad",
"logs.filter.protocol.all": "Alle Pfade",
"logs.filter.protocol.none": "Keine Pfaddaten",
"logs.protocol.mode.native": "Nativ",
"logs.protocol.mode.translated": "Übersetzt",
"logs.protocol.mode.legacyBridge": "Legacy-Brücke",
"logs.protocol.mode.blocked": "Blockiert",
"logs.protocol.disposition.passthrough": "Durchgereicht",
"logs.protocol.disposition.translated": "Übersetzt",
"logs.protocol.disposition.degraded": "Eingeschränkt",
"logs.protocol.disposition.unsupported": "Verworfen",
"logs.protocol.wire.responses": "Responses",
"logs.protocol.wire.chat": "Chat",
"logs.protocol.wire.messages": "Messages",
"logs.protocol.wire.other": "Anderer Adapter",
"logs.protocol.hop.ir": "IR",
"logs.protocol.hop.internal": "Responses (intern)",
"logs.protocol.badgeTitle": "Protokollpfad: {path} ({mode})",
"logs.detail.protocol.section": "Protokollpfad",
"logs.detail.protocol.inbound": "Client-API",
"logs.detail.protocol.mode": "Zustellmodus",
"logs.detail.protocol.upstream": "Upstream-Format",
"logs.detail.protocol.requestPath": "Anfragepfad",
"logs.detail.protocol.responsePath": "Antwortpfad",
"logs.detail.protocol.reasons": "Gründe",
"logs.detail.protocol.features": "Auswirkungen auf Funktionen",
"logs.detail.protocol.attempts": "Pfade der Versuche",
"logs.detail.protocol.attempt": "Versuch {ordinal}",
"logs.detail.protocol.none": "Für diese Anfrage wurden keine Pfaddaten aufgezeichnet.",
"logs.detail.section.performance": "Leistung",
"logs.detail.section.cost": "API-Listenpreis-Äquivalent",
"logs.detail.section.attempts": "Combo-Versuche",
Expand Down
29 changes: 29 additions & 0 deletions gui/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -926,6 +926,35 @@ export const en = {
"logs.detail.route.selected": "Selected",
"logs.detail.route.candidates": "Candidates",
"logs.detail.route.unknown": "No route trace recorded for this request (pre-trace row).",
"logs.filter.protocol.label": "Protocol path",
"logs.filter.protocol.all": "All paths",
"logs.filter.protocol.none": "No path data",
"logs.protocol.mode.native": "Native",
"logs.protocol.mode.translated": "Translated",
"logs.protocol.mode.legacyBridge": "Legacy bridge",
"logs.protocol.mode.blocked": "Blocked",
"logs.protocol.disposition.passthrough": "Passed through",
"logs.protocol.disposition.translated": "Translated",
"logs.protocol.disposition.degraded": "Degraded",
"logs.protocol.disposition.unsupported": "Dropped",
"logs.protocol.wire.responses": "Responses",
"logs.protocol.wire.chat": "Chat",
"logs.protocol.wire.messages": "Messages",
"logs.protocol.wire.other": "Other adapter",
"logs.protocol.hop.ir": "IR",
"logs.protocol.hop.internal": "Responses (internal)",
"logs.protocol.badgeTitle": "Protocol path: {path} ({mode})",
"logs.detail.protocol.section": "Protocol path",
"logs.detail.protocol.inbound": "Client API",
"logs.detail.protocol.mode": "Delivery mode",
"logs.detail.protocol.upstream": "Upstream wire",
"logs.detail.protocol.requestPath": "Request path",
"logs.detail.protocol.responsePath": "Response path",
"logs.detail.protocol.reasons": "Reasons",
"logs.detail.protocol.features": "Feature effects",
"logs.detail.protocol.attempts": "Attempt paths",
"logs.detail.protocol.attempt": "Attempt {ordinal}",
"logs.detail.protocol.none": "No path data recorded for this request.",
"logs.detail.section.performance": "Performance",
"logs.detail.section.cost": "API list-price equivalent",
"logs.detail.section.attempts": "Combo attempts",
Expand Down
29 changes: 29 additions & 0 deletions gui/src/i18n/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -907,6 +907,35 @@ export const fr: Record<TKey, string> = {
"logs.detail.route.selected": "Sélection",
"logs.detail.route.candidates": "Candidats",
"logs.detail.route.unknown": "Aucune trace de routage enregistrée pour cette requête (ligne antérieure à la traçabilité).",
"logs.filter.protocol.label": "Chemin de protocole",
"logs.filter.protocol.all": "Tous les chemins",
"logs.filter.protocol.none": "Aucune donnée de chemin",
"logs.protocol.mode.native": "Natif",
"logs.protocol.mode.translated": "Traduit",
"logs.protocol.mode.legacyBridge": "Pont hérité",
"logs.protocol.mode.blocked": "Bloqué",
"logs.protocol.disposition.passthrough": "Transmis tel quel",
"logs.protocol.disposition.translated": "Traduit",
"logs.protocol.disposition.degraded": "Dégradé",
"logs.protocol.disposition.unsupported": "Abandonné",
"logs.protocol.wire.responses": "Responses",
"logs.protocol.wire.chat": "Chat",
"logs.protocol.wire.messages": "Messages",
"logs.protocol.wire.other": "Autre adaptateur",
"logs.protocol.hop.ir": "IR",
"logs.protocol.hop.internal": "Responses (interne)",
"logs.protocol.badgeTitle": "Chemin de protocole : {path} ({mode})",
"logs.detail.protocol.section": "Chemin de protocole",
"logs.detail.protocol.inbound": "API client",
"logs.detail.protocol.mode": "Mode de livraison",
"logs.detail.protocol.upstream": "Format amont",
"logs.detail.protocol.requestPath": "Chemin de la requête",
"logs.detail.protocol.responsePath": "Chemin de la réponse",
"logs.detail.protocol.reasons": "Raisons",
"logs.detail.protocol.features": "Effets sur les fonctionnalités",
"logs.detail.protocol.attempts": "Chemins des tentatives",
"logs.detail.protocol.attempt": "Tentative {ordinal}",
"logs.detail.protocol.none": "Aucune donnée de chemin enregistrée pour cette requête.",
"logs.detail.section.performance": "Performances",
"logs.detail.section.cost": "Équivalent au tarif catalogue de l’API",
"logs.detail.section.attempts": "Tentatives de combinaison",
Expand Down
29 changes: 29 additions & 0 deletions gui/src/i18n/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -837,6 +837,35 @@ export const ja: Record<TKey, string> = {
"logs.detail.route.selected": "選択済み",
"logs.detail.route.candidates": "候補",
"logs.detail.route.unknown": "このリクエストにはルートトレースが記録されていません(トレース前の行)。",
"logs.filter.protocol.label": "プロトコル経路",
"logs.filter.protocol.all": "すべての経路",
"logs.filter.protocol.none": "経路データなし",
"logs.protocol.mode.native": "ネイティブ",
"logs.protocol.mode.translated": "変換",
"logs.protocol.mode.legacyBridge": "レガシーブリッジ",
"logs.protocol.mode.blocked": "ブロック",
"logs.protocol.disposition.passthrough": "そのまま転送",
"logs.protocol.disposition.translated": "変換",
"logs.protocol.disposition.degraded": "劣化",
"logs.protocol.disposition.unsupported": "破棄",
"logs.protocol.wire.responses": "Responses",
"logs.protocol.wire.chat": "Chat",
"logs.protocol.wire.messages": "Messages",
"logs.protocol.wire.other": "その他のアダプター",
"logs.protocol.hop.ir": "IR",
"logs.protocol.hop.internal": "Responses(内部)",
"logs.protocol.badgeTitle": "プロトコル経路: {path}({mode})",
"logs.detail.protocol.section": "プロトコル経路",
"logs.detail.protocol.inbound": "クライアント API",
"logs.detail.protocol.mode": "配信モード",
"logs.detail.protocol.upstream": "アップストリーム形式",
"logs.detail.protocol.requestPath": "リクエスト経路",
"logs.detail.protocol.responsePath": "レスポンス経路",
"logs.detail.protocol.reasons": "理由",
"logs.detail.protocol.features": "機能への影響",
"logs.detail.protocol.attempts": "試行ごとの経路",
"logs.detail.protocol.attempt": "試行 {ordinal}",
"logs.detail.protocol.none": "このリクエストには経路データが記録されていません。",
"logs.detail.section.performance": "パフォーマンス",
"logs.detail.section.cost": "API 定価相当額",
"logs.detail.section.attempts": "コンボの試行",
Expand Down
29 changes: 29 additions & 0 deletions gui/src/i18n/ko.ts
Original file line number Diff line number Diff line change
Expand Up @@ -908,6 +908,35 @@ export const ko: Record<TKey, string> = {
"logs.detail.route.selected": "선택됨",
"logs.detail.route.candidates": "후보",
"logs.detail.route.unknown": "이 요청에 대한 라우팅 추적이 기록되지 않았습니다(추적 이전 행).",
"logs.filter.protocol.label": "프로토콜 경로",
"logs.filter.protocol.all": "모든 경로",
"logs.filter.protocol.none": "경로 데이터 없음",
"logs.protocol.mode.native": "네이티브",
"logs.protocol.mode.translated": "변환됨",
"logs.protocol.mode.legacyBridge": "레거시 브리지",
"logs.protocol.mode.blocked": "차단됨",
"logs.protocol.disposition.passthrough": "그대로 전달",
"logs.protocol.disposition.translated": "변환됨",
"logs.protocol.disposition.degraded": "저하됨",
"logs.protocol.disposition.unsupported": "삭제됨",
"logs.protocol.wire.responses": "Responses",
"logs.protocol.wire.chat": "Chat",
"logs.protocol.wire.messages": "Messages",
"logs.protocol.wire.other": "기타 어댑터",
"logs.protocol.hop.ir": "IR",
"logs.protocol.hop.internal": "Responses (내부)",
"logs.protocol.badgeTitle": "프로토콜 경로: {path} ({mode})",
"logs.detail.protocol.section": "프로토콜 경로",
"logs.detail.protocol.inbound": "클라이언트 API",
"logs.detail.protocol.mode": "전달 모드",
"logs.detail.protocol.upstream": "업스트림 형식",
"logs.detail.protocol.requestPath": "요청 경로",
"logs.detail.protocol.responsePath": "응답 경로",
"logs.detail.protocol.reasons": "사유",
"logs.detail.protocol.features": "기능 영향",
"logs.detail.protocol.attempts": "시도별 경로",
"logs.detail.protocol.attempt": "시도 {ordinal}",
"logs.detail.protocol.none": "이 요청에는 기록된 경로 데이터가 없습니다.",
"logs.detail.section.performance": "성능",
"logs.detail.section.cost": "API 정가 환산치",
"logs.detail.section.attempts": "Combo 시도",
Expand Down
29 changes: 29 additions & 0 deletions gui/src/i18n/ru.ts
Original file line number Diff line number Diff line change
Expand Up @@ -894,6 +894,35 @@ export const ru: Record<TKey, string> = {
"logs.detail.route.selected": "Выбрано",
"logs.detail.route.candidates": "Кандидаты",
"logs.detail.route.unknown": "Для этого запроса трасса маршрута не записана (строка до трассировки).",
"logs.filter.protocol.label": "Путь протокола",
"logs.filter.protocol.all": "Все пути",
"logs.filter.protocol.none": "Нет данных о пути",
"logs.protocol.mode.native": "Нативный",
"logs.protocol.mode.translated": "Преобразованный",
"logs.protocol.mode.legacyBridge": "Устаревший мост",
"logs.protocol.mode.blocked": "Заблокирован",
"logs.protocol.disposition.passthrough": "Передано без изменений",
"logs.protocol.disposition.translated": "Преобразовано",
"logs.protocol.disposition.degraded": "Ухудшено",
"logs.protocol.disposition.unsupported": "Отброшено",
"logs.protocol.wire.responses": "Responses",
"logs.protocol.wire.chat": "Chat",
"logs.protocol.wire.messages": "Messages",
"logs.protocol.wire.other": "Другой адаптер",
"logs.protocol.hop.ir": "IR",
"logs.protocol.hop.internal": "Responses (внутренний)",
"logs.protocol.badgeTitle": "Путь протокола: {path} ({mode})",
"logs.detail.protocol.section": "Путь протокола",
"logs.detail.protocol.inbound": "Клиентский API",
"logs.detail.protocol.mode": "Режим доставки",
"logs.detail.protocol.upstream": "Формат upstream",
"logs.detail.protocol.requestPath": "Путь запроса",
"logs.detail.protocol.responsePath": "Путь ответа",
"logs.detail.protocol.reasons": "Причины",
"logs.detail.protocol.features": "Влияние на функции",
"logs.detail.protocol.attempts": "Пути попыток",
"logs.detail.protocol.attempt": "Попытка {ordinal}",
"logs.detail.protocol.none": "Для этого запроса данные о пути не записаны.",
"logs.detail.section.performance": "Производительность",
"logs.detail.section.cost": "Эквивалент стоимости по прайс-листу API",
"logs.detail.section.attempts": "Попытки комбо",
Expand Down
Loading
Loading