diff --git a/haiku_rag_slim/haiku/rag/ingester/api/static/index.html b/haiku_rag_slim/haiku/rag/ingester/api/static/index.html
index 8b598bf2..7f740f5b 100644
--- a/haiku_rag_slim/haiku/rag/ingester/api/static/index.html
+++ b/haiku_rag_slim/haiku/rag/ingester/api/static/index.html
@@ -136,9 +136,35 @@
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
}
.breakdown span { margin-right: 12px; }
+
+ /* Floating tooltip for truncated cells. Triggered via data-tip
+ on any element; positioned in JS so it escapes the table cell's
+ overflow:hidden context. */
+ #tooltip {
+ position: fixed;
+ z-index: 1000;
+ max-width: 480px;
+ padding: 6px 10px;
+ background: #0a0d11;
+ color: var(--text);
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ font-size: 12px;
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
+ line-height: 1.4;
+ word-break: break-all;
+ white-space: pre-wrap;
+ pointer-events: none;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
+ opacity: 0;
+ transition: opacity 0.08s ease-out;
+ }
+ #tooltip.visible { opacity: 1; }
+ [data-tip] { cursor: help; }
+
@@ -377,7 +403,7 @@
? SKIP_REASON_LABELS[reason] || reason
: null;
const skipBadge = reason
- ? ` ${escapeHtml(label)}`
+ ? ` ${escapeHtml(label)}`
: "";
const polledCell = s.last_polled_at
? relTime(s.last_polled_at) + skipBadge
@@ -437,9 +463,9 @@
const detailClass = p.reachable ? "ok" : "err";
return `
| docling-serve |
- ${escapeHtml(p.base_url)} |
+ ${escapeHtml(p.base_url)} |
${badge} |
- ${detail} |
+ ${detail} |
`;
})
.join("");
@@ -459,17 +485,17 @@
}
const rows = jobs
.map((j) => {
- const tryTitle = j.last_error
- ? ` title="Previous attempt failed: ${escapeHtml(j.last_error)}"`
+ const tryTip = j.last_error
+ ? ` data-tip="Previous attempt failed: ${escapeHtml(j.last_error)}"`
: "";
const tryCell =
j.attempts > 1
- ? `${j.attempts}/${j.max_attempts} | `
+ ? `${j.attempts}/${j.max_attempts} | `
: `${j.attempts}/${j.max_attempts} | `;
return `
- | ${shortId(j.id)} |
+ ${shortId(j.id)} |
${opBadge(j.op)} |
- ${escapeHtml(j.uri)} |
+ ${escapeHtml(j.uri)} |
${escapeHtml(j.claimed_by ?? "—")} |
${tryCell}
${relTime(j.claimed_at)} |
@@ -497,9 +523,9 @@
const rows = jobs
.map(
(j) => `
- | ${shortId(j.id)} |
- ${escapeHtml(j.uri)} |
- ${escapeHtml(j.last_error ?? "")} |
+ ${shortId(j.id)} |
+ ${escapeHtml(j.uri)} |
+ ${escapeHtml(j.last_error ?? "")} |
${relTime(j.completed_at)} |
@@ -530,7 +556,7 @@
}
return ` |
| ${opBadge(j.op)} |
- ${escapeHtml(j.uri)} |
+ ${escapeHtml(j.uri)} |
${escapeHtml(j.source_id)} |
${duration} |
${relTime(j.completed_at)} |
@@ -561,7 +587,7 @@
let detail = `${health.worker_count} workers · ${health.poller_count} pollers`;
if (health.worker_breaker_open) {
const fails = health.worker_breaker_consecutive_failures;
- detail += ` · WORKER BREAKER OPEN`;
+ detail += ` · WORKER BREAKER OPEN`;
}
$("health-detail").innerHTML = detail;
renderChips(health.queue_counts);
@@ -605,6 +631,43 @@
refresh();
};
+ // Floating tooltip: shows the full content of any [data-tip] element
+ // immediately on hover. Uses event delegation so it survives the
+ // table-body innerHTML reset each refresh().
+ (() => {
+ const tip = $("tooltip");
+ let current = null;
+ const place = (e) => {
+ const pad = 12;
+ const w = tip.offsetWidth;
+ const h = tip.offsetHeight;
+ let x = e.clientX + pad;
+ let y = e.clientY + pad;
+ if (x + w > window.innerWidth - 8) x = e.clientX - w - pad;
+ if (y + h > window.innerHeight - 8) y = e.clientY - h - pad;
+ tip.style.left = `${Math.max(8, x)}px`;
+ tip.style.top = `${Math.max(8, y)}px`;
+ };
+ document.addEventListener("mouseover", (e) => {
+ const el = e.target.closest("[data-tip]");
+ if (!el || el === current) return;
+ current = el;
+ tip.textContent = el.getAttribute("data-tip") || "";
+ tip.classList.add("visible");
+ place(e);
+ });
+ document.addEventListener("mousemove", (e) => {
+ if (current) place(e);
+ });
+ document.addEventListener("mouseout", (e) => {
+ if (!current) return;
+ const next = e.relatedTarget && e.relatedTarget.closest?.("[data-tip]");
+ if (next === current) return;
+ tip.classList.remove("visible");
+ current = null;
+ });
+ })();
+
setInterval(updateLastRefresh, 1000);
refresh();
setInterval(refresh, POLL_MS);